@banaxi/banana-code 1.5.1 → 1.7.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 +68 -1
- package/package.json +5 -2
- package/src/config.js +1 -0
- package/src/constants.js +3 -2
- package/src/index.js +183 -5
- package/src/permissions.js +18 -0
- package/src/prompt.js +50 -0
- package/src/providers/claude.js +6 -0
- package/src/providers/gemini.js +13 -2
- package/src/providers/mistral.js +6 -0
- package/src/providers/ollama.js +6 -0
- package/src/providers/ollamaCloud.js +11 -1
- package/src/providers/openai.js +18 -2
- package/src/server.js +147 -0
- package/src/tools/memoryTools.js +46 -0
- package/src/tools/readManyFiles.js +32 -0
- package/src/tools/registry.js +68 -2
- package/src/utils/memory.js +52 -0
package/README.md
CHANGED
|
@@ -62,12 +62,14 @@ While tools like Cursor provide great GUI experiences, Banana Code is built for
|
|
|
62
62
|
|
|
63
63
|
## 🚀 Installation
|
|
64
64
|
|
|
65
|
-
Install Banana Code globally via npm:
|
|
65
|
+
Install Banana Code globally via npm using the scoped package name:
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
npm install -g @banaxi/banana-code
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
> **⚠️ Important Notice:** Please ensure you install `@banaxi/banana-code`. The unscoped `banana-code` package on npm is NOT affiliated with this project.
|
|
72
|
+
|
|
71
73
|
## 🛠️ Setup
|
|
72
74
|
|
|
73
75
|
On your first run, Banana Code will walk you through a quick setup to configure your preferred AI providers:
|
|
@@ -97,6 +99,8 @@ While in a chat, use these special commands:
|
|
|
97
99
|
- `/model`: Switch the active AI model on the fly.
|
|
98
100
|
- `/chats`: Open an interactive menu to view and resume past auto-titled chat sessions.
|
|
99
101
|
- `/clean`: Compress your current chat history into a dense summary to save tokens.
|
|
102
|
+
- `/memory`: Manage your global AI memories (facts the AI remembers across all projects).
|
|
103
|
+
- `/init`: Analyze the current project and generate a `BANANA.md` summary for instant context.
|
|
100
104
|
- `/context`: View your current message count and estimated token usage.
|
|
101
105
|
- `/settings`: Toggle UI features like syntax highlighting and auto-workspace feeding.
|
|
102
106
|
- `/plan` & `/agent`: Toggle between Plan & Execute mode and standard Agent mode.
|
|
@@ -155,6 +159,69 @@ Banana Code supports the open standard [Model Context Protocol](https://modelcon
|
|
|
155
159
|
|
|
156
160
|
Restart Banana Code, and the AI will instantly know how to use these new tools natively!
|
|
157
161
|
|
|
162
|
+
### 🧠 Global AI Memory
|
|
163
|
+
Banana Code features a persistent "brain" that remembers your preferences across every project you work on.
|
|
164
|
+
|
|
165
|
+
1. Enable **Enable Global AI Memory** in the `/settings` menu.
|
|
166
|
+
2. Tell the AI facts about yourself or your coding style (e.g., "My name is Max" or "I prefer using Python for data scripts").
|
|
167
|
+
3. Use the `/memory` command to view, manually add, or delete saved facts.
|
|
168
|
+
4. The AI will now automatically adhere to these preferences in every future session!
|
|
169
|
+
|
|
170
|
+
### 🍌 Project Initialization (`/init`)
|
|
171
|
+
Stop repeating yourself! When you start working in a new folder, type `/init`.
|
|
172
|
+
|
|
173
|
+
Banana Code will analyze your entire project structure and generate a **`BANANA.md`** file. This file acts as a high-level architectural summary. Every time you start `banana` in that folder, the AI silently reads this file, giving it instant context about your project's goals and technologies from the very first message.
|
|
174
|
+
|
|
175
|
+
## 📡 Headless API Mode (`--api`)
|
|
176
|
+
Banana Code can be run as a background engine, exposing its powerful tool-calling and provider-switching logic via a local HTTP and WebSocket server. This allows you to build custom GUIs (Electron, Tauri, React) on top of the Banana Code engine without rewriting any AI logic.
|
|
177
|
+
|
|
178
|
+
Start the server:
|
|
179
|
+
```bash
|
|
180
|
+
banana --api 4000
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### HTTP Endpoints
|
|
184
|
+
- `GET /api/status`: Returns engine status, active provider, and model.
|
|
185
|
+
- `GET /api/sessions`: Returns a JSON array of all saved chat sessions.
|
|
186
|
+
- `GET /api/config`: Returns the current `config.json` preferences.
|
|
187
|
+
|
|
188
|
+
### WebSocket Streaming & Chat
|
|
189
|
+
Connect a WebSocket client (like `wscat` or your GUI frontend) to `ws://localhost:4000`.
|
|
190
|
+
|
|
191
|
+
**Send a chat message:**
|
|
192
|
+
```json
|
|
193
|
+
{ "type": "chat", "text": "Run the sensors command" }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Streamed Response Format:**
|
|
197
|
+
Banana Code streams data back to the client in real-time chunks:
|
|
198
|
+
- `{"type": "chunk", "content": "The output of the command is..."}`
|
|
199
|
+
- `{"type": "tool_start", "tool": "execute_command"}`
|
|
200
|
+
- `{"type": "done", "finalResponse": "..."}`
|
|
201
|
+
|
|
202
|
+
### Remote Tool Approval (Security)
|
|
203
|
+
If the AI decides to run a shell command or patch a file, Banana Code pauses execution and sends a permission ticket to your GUI:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"type": "permission_requested",
|
|
208
|
+
"ticketId": "5c9b2a...",
|
|
209
|
+
"action": "Execute Command",
|
|
210
|
+
"details": "sensors"
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Your GUI must present a dialog to the user and respond with the same `ticketId` to resume execution:
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"type": "permission_response",
|
|
218
|
+
"ticketId": "5c9b2a...",
|
|
219
|
+
"allowed": true,
|
|
220
|
+
"session": true
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
If an invalid `ticketId` is provided, Banana Code automatically blocks the tool execution to ensure safety.
|
|
224
|
+
|
|
158
225
|
## 🔐 Privacy & Security
|
|
159
226
|
|
|
160
227
|
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.7.0",
|
|
4
4
|
"description": "🍌 BananaCode",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"banana",
|
|
@@ -32,12 +32,15 @@
|
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
33
33
|
"@openai/codex": "^0.117.0",
|
|
34
34
|
"chalk": "^5.4.1",
|
|
35
|
+
"cors": "^2.8.6",
|
|
35
36
|
"diff": "^8.0.4",
|
|
37
|
+
"express": "^5.2.1",
|
|
36
38
|
"glob": "13.0.6",
|
|
37
39
|
"marked": "^15.0.12",
|
|
38
40
|
"marked-terminal": "^7.3.0",
|
|
39
41
|
"open": "^11.0.0",
|
|
40
42
|
"openai": "^4.79.1",
|
|
41
|
-
"ora": "^8.1.1"
|
|
43
|
+
"ora": "^8.1.1",
|
|
44
|
+
"ws": "^8.20.0"
|
|
42
45
|
}
|
|
43
46
|
}
|
package/src/config.js
CHANGED
|
@@ -186,6 +186,7 @@ async function runSetupWizard() {
|
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
const config = await setupProvider(provider);
|
|
189
|
+
config.useMemory = true;
|
|
189
190
|
|
|
190
191
|
await saveConfig(config);
|
|
191
192
|
console.log(chalk.yellow.bold("\nYou're all peeled and ready. Type your first message!\n"));
|
package/src/constants.js
CHANGED
|
@@ -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';
|
|
@@ -16,6 +16,7 @@ import { loadSession, saveSession, generateSessionId, getLatestSessionId, listSe
|
|
|
16
16
|
import { printMarkdown } from './utils/markdown.js';
|
|
17
17
|
import { estimateConversationTokens } from './utils/tokens.js';
|
|
18
18
|
import { mcpManager } from './utils/mcp.js';
|
|
19
|
+
import { startApiServer } from './server.js';
|
|
19
20
|
|
|
20
21
|
let config;
|
|
21
22
|
let providerInstance;
|
|
@@ -311,17 +312,29 @@ async function handleSlashCommand(command) {
|
|
|
311
312
|
value: 'useMarkedTerminal',
|
|
312
313
|
checked: config.useMarkedTerminal || false
|
|
313
314
|
},
|
|
315
|
+
{
|
|
316
|
+
name: 'Enable Surgical File Patching (patch_file tool)',
|
|
317
|
+
value: 'usePatchFile',
|
|
318
|
+
checked: config.usePatchFile !== false
|
|
319
|
+
},
|
|
314
320
|
{
|
|
315
321
|
name: 'Always show current token count in status bar',
|
|
316
322
|
value: 'showTokenCount',
|
|
317
323
|
checked: config.showTokenCount || false
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: 'Enable Global AI Memory (Allows AI to save facts persistently)',
|
|
327
|
+
value: 'useMemory',
|
|
328
|
+
checked: config.useMemory !== false
|
|
318
329
|
}
|
|
319
330
|
]
|
|
320
331
|
});
|
|
321
332
|
|
|
322
333
|
config.autoFeedWorkspace = enabledSettings.includes('autoFeedWorkspace');
|
|
323
334
|
config.useMarkedTerminal = enabledSettings.includes('useMarkedTerminal');
|
|
335
|
+
config.usePatchFile = enabledSettings.includes('usePatchFile');
|
|
324
336
|
config.showTokenCount = enabledSettings.includes('showTokenCount');
|
|
337
|
+
config.useMemory = enabledSettings.includes('useMemory');
|
|
325
338
|
await saveConfig(config);
|
|
326
339
|
if (providerInstance) {
|
|
327
340
|
const savedMessages = providerInstance.messages;
|
|
@@ -362,6 +375,8 @@ async function handleSlashCommand(command) {
|
|
|
362
375
|
break;
|
|
363
376
|
case '/plan':
|
|
364
377
|
config.planMode = true;
|
|
378
|
+
config.askMode = false;
|
|
379
|
+
config.securityMode = false;
|
|
365
380
|
await saveConfig(config);
|
|
366
381
|
if (providerInstance) {
|
|
367
382
|
const savedMessages = providerInstance.messages;
|
|
@@ -372,8 +387,52 @@ async function handleSlashCommand(command) {
|
|
|
372
387
|
}
|
|
373
388
|
console.log(chalk.magenta(`Plan mode enabled. For significant changes, the AI will now propose an implementation plan before writing code.`));
|
|
374
389
|
break;
|
|
390
|
+
case '/ask':
|
|
391
|
+
config.askMode = true;
|
|
392
|
+
config.planMode = false;
|
|
393
|
+
config.securityMode = false;
|
|
394
|
+
await saveConfig(config);
|
|
395
|
+
if (providerInstance) {
|
|
396
|
+
const savedMessages = providerInstance.messages;
|
|
397
|
+
providerInstance = createProvider();
|
|
398
|
+
providerInstance.messages = savedMessages;
|
|
399
|
+
} else {
|
|
400
|
+
providerInstance = createProvider();
|
|
401
|
+
}
|
|
402
|
+
console.log(chalk.blue(`Ask mode enabled. The AI will only answer questions and cannot edit files.`));
|
|
403
|
+
break;
|
|
404
|
+
case '/security':
|
|
405
|
+
config.securityMode = true;
|
|
406
|
+
config.askMode = false;
|
|
407
|
+
config.planMode = false;
|
|
408
|
+
await saveConfig(config);
|
|
409
|
+
if (providerInstance) {
|
|
410
|
+
const savedMessages = providerInstance.messages;
|
|
411
|
+
providerInstance = createProvider();
|
|
412
|
+
providerInstance.messages = savedMessages;
|
|
413
|
+
} else {
|
|
414
|
+
providerInstance = createProvider();
|
|
415
|
+
}
|
|
416
|
+
console.log(chalk.red(`Security mode enabled. The AI will look for and help fix vulnerabilities.`));
|
|
417
|
+
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.`));
|
|
418
|
+
break;
|
|
419
|
+
case '/yolo':
|
|
420
|
+
config.yolo = !config.yolo;
|
|
421
|
+
setYoloMode(config.yolo);
|
|
422
|
+
await saveConfig(config);
|
|
423
|
+
if (providerInstance) {
|
|
424
|
+
const savedMessages = providerInstance.messages;
|
|
425
|
+
providerInstance = createProvider();
|
|
426
|
+
providerInstance.messages = savedMessages;
|
|
427
|
+
} else {
|
|
428
|
+
providerInstance = createProvider();
|
|
429
|
+
}
|
|
430
|
+
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'));
|
|
431
|
+
break;
|
|
375
432
|
case '/agent':
|
|
376
433
|
config.planMode = false;
|
|
434
|
+
config.askMode = false;
|
|
435
|
+
config.securityMode = false;
|
|
377
436
|
await saveConfig(config);
|
|
378
437
|
if (providerInstance) {
|
|
379
438
|
const savedMessages = providerInstance.messages;
|
|
@@ -424,6 +483,91 @@ async function handleSlashCommand(command) {
|
|
|
424
483
|
}
|
|
425
484
|
}
|
|
426
485
|
break;
|
|
486
|
+
case '/memory':
|
|
487
|
+
if (config.useMemory === false) {
|
|
488
|
+
console.log(chalk.yellow("Global AI Memory is disabled. Enable it in /settings first."));
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
const { select: memSelect, input: memInput } = await import('@inquirer/prompts');
|
|
492
|
+
const { loadMemory, removeMemory, addMemory } = await import('./utils/memory.js');
|
|
493
|
+
let memAction = await memSelect({
|
|
494
|
+
message: 'Manage Global AI Memory:',
|
|
495
|
+
choices: [
|
|
496
|
+
{ name: 'View all memories', value: 'view' },
|
|
497
|
+
{ name: 'Add a new memory manually', value: 'add' },
|
|
498
|
+
{ name: 'Delete a memory', value: 'delete' }
|
|
499
|
+
],
|
|
500
|
+
loop: false
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
if (memAction === 'view') {
|
|
504
|
+
const mems = await loadMemory();
|
|
505
|
+
if (mems.length === 0) {
|
|
506
|
+
console.log(chalk.yellow("No global memories saved yet."));
|
|
507
|
+
} else {
|
|
508
|
+
console.log(chalk.cyan.bold("\nGlobal Memories:"));
|
|
509
|
+
mems.forEach(m => {
|
|
510
|
+
console.log(chalk.gray(`[${m.id}] `) + chalk.green(m.fact));
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
} else if (memAction === 'add') {
|
|
514
|
+
const newFact = await memInput({
|
|
515
|
+
message: 'Enter the fact you want the AI to remember globally:',
|
|
516
|
+
validate: (v) => v.trim().length > 0 || 'Memory cannot be empty'
|
|
517
|
+
});
|
|
518
|
+
const id = await addMemory(newFact);
|
|
519
|
+
console.log(chalk.green(`Memory saved with ID: ${id}`));
|
|
520
|
+
providerInstance = createProvider(); // Reload provider to inject new memory
|
|
521
|
+
} else if (memAction === 'delete') {
|
|
522
|
+
const mems = await loadMemory();
|
|
523
|
+
if (mems.length === 0) {
|
|
524
|
+
console.log(chalk.yellow("No memories to delete."));
|
|
525
|
+
} else {
|
|
526
|
+
const idToDelete = await memSelect({
|
|
527
|
+
message: 'Select a memory to delete:',
|
|
528
|
+
choices: mems.map(m => ({ name: m.fact, value: m.id })),
|
|
529
|
+
loop: false,
|
|
530
|
+
pageSize: 10
|
|
531
|
+
});
|
|
532
|
+
const success = await removeMemory(idToDelete);
|
|
533
|
+
if (success) {
|
|
534
|
+
console.log(chalk.green(`Memory deleted.`));
|
|
535
|
+
providerInstance = createProvider(); // Reload provider
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
case '/init':
|
|
541
|
+
console.log(chalk.cyan("Generating project summary for BANANA.md..."));
|
|
542
|
+
const initSpinner = ora({ text: 'Analyzing project...', color: 'yellow', stream: process.stdout }).start();
|
|
543
|
+
try {
|
|
544
|
+
const { getWorkspaceTree } = await import('./utils/workspace.js');
|
|
545
|
+
const tree = await getWorkspaceTree();
|
|
546
|
+
|
|
547
|
+
const initProvider = createProvider();
|
|
548
|
+
// We use a completely blank slate for this so it doesn't get confused
|
|
549
|
+
initProvider.messages = [];
|
|
550
|
+
|
|
551
|
+
let initPrompt = "SYSTEM: You are a project summarizer. Review the following project file tree and briefly describe what this project is, what technologies it uses, and any obvious conventions. Keep it under 2 paragraphs. Output ONLY the summary text.";
|
|
552
|
+
initPrompt += `\n\n--- Project Tree ---\n${tree}`;
|
|
553
|
+
|
|
554
|
+
const summary = await initProvider.sendMessage(initPrompt);
|
|
555
|
+
|
|
556
|
+
const fs = await import('fs/promises');
|
|
557
|
+
const path = await import('path');
|
|
558
|
+
const bananaPath = path.join(process.cwd(), 'BANANA.md');
|
|
559
|
+
await fs.writeFile(bananaPath, summary, 'utf8');
|
|
560
|
+
|
|
561
|
+
initSpinner.stop();
|
|
562
|
+
console.log(chalk.green(`Successfully created BANANA.md!`));
|
|
563
|
+
|
|
564
|
+
// Re-init current provider so it picks up the new BANANA.md
|
|
565
|
+
providerInstance = createProvider();
|
|
566
|
+
} catch (err) {
|
|
567
|
+
initSpinner.stop();
|
|
568
|
+
console.log(chalk.red(`Failed to initialize project: ${err.message}`));
|
|
569
|
+
}
|
|
570
|
+
break;
|
|
427
571
|
case '/help':
|
|
428
572
|
console.log(chalk.yellow(`
|
|
429
573
|
Available commands:
|
|
@@ -437,8 +581,11 @@ Available commands:
|
|
|
437
581
|
/beta - Manage beta features and tools
|
|
438
582
|
/settings - Manage app settings (workspace auto-feed, etc)
|
|
439
583
|
/skills - List loaded agent skills
|
|
584
|
+
/memory - Manage global AI memories
|
|
585
|
+
/init - Generate a BANANA.md project summary file
|
|
440
586
|
/plan - Enable Plan Mode (AI proposes a plan for big changes)
|
|
441
587
|
/agent - Enable Agent Mode (default, AI edits directly)
|
|
588
|
+
/yolo - Toggle YOLO mode (skip all permission requests)
|
|
442
589
|
/debug - Toggle debug mode (show tool results)
|
|
443
590
|
/help - Show all commands
|
|
444
591
|
/exit - Quit Banana Code
|
|
@@ -567,7 +714,10 @@ function drawPromptBox(inputText, cursorPos) {
|
|
|
567
714
|
// Redraw status bar and separator (they are always below the prompt)
|
|
568
715
|
const modelDisplay = providerInstance ? providerInstance.modelName : (config.model || 'unknown');
|
|
569
716
|
const providerDisplay = config.provider.toUpperCase();
|
|
570
|
-
|
|
717
|
+
let modeDisplay = chalk.green('AGENT MODE');
|
|
718
|
+
if (config.askMode) modeDisplay = chalk.blue('ASK MODE');
|
|
719
|
+
else if (config.securityMode) modeDisplay = chalk.red('SECURITY MODE');
|
|
720
|
+
else if (config.planMode) modeDisplay = chalk.magenta('PLAN MODE');
|
|
571
721
|
|
|
572
722
|
let tokenDisplay = '';
|
|
573
723
|
if (config.showTokenCount && providerInstance) {
|
|
@@ -585,7 +735,8 @@ function drawPromptBox(inputText, cursorPos) {
|
|
|
585
735
|
tokenDisplay = ` / Tokens: ${color(tokens.toLocaleString())}`;
|
|
586
736
|
}
|
|
587
737
|
|
|
588
|
-
const
|
|
738
|
+
const yoloDisplay = config.yolo ? chalk.bgRed.white.bold(' ⚠️ YOLO ') : '';
|
|
739
|
+
const leftText = ` Provider: ${chalk.cyan(providerDisplay)} / Model: ${chalk.yellow(modelDisplay)} / ${modeDisplay}${tokenDisplay}${yoloDisplay ? ' / ' + yoloDisplay : ''}`;
|
|
589
740
|
const rightText = '/help for shortcuts ';
|
|
590
741
|
const leftStripped = leftText.replace(/\x1b\[[0-9;]*m/g, '');
|
|
591
742
|
const midPad = Math.max(0, width - leftStripped.length - rightText.length);
|
|
@@ -628,7 +779,10 @@ function drawPromptBoxInitial(inputText) {
|
|
|
628
779
|
// Status bar: Current Provider / Model + right-aligned "/help for shortcuts"
|
|
629
780
|
const modelDisplay = providerInstance ? providerInstance.modelName : (config.model || 'unknown');
|
|
630
781
|
const providerDisplay = config.provider.toUpperCase();
|
|
631
|
-
|
|
782
|
+
let modeDisplay = chalk.green('AGENT MODE');
|
|
783
|
+
if (config.askMode) modeDisplay = chalk.blue('ASK MODE');
|
|
784
|
+
else if (config.securityMode) modeDisplay = chalk.red('SECURITY MODE');
|
|
785
|
+
else if (config.planMode) modeDisplay = chalk.magenta('PLAN MODE');
|
|
632
786
|
|
|
633
787
|
let tokenDisplay = '';
|
|
634
788
|
if (config.showTokenCount && providerInstance) {
|
|
@@ -646,7 +800,8 @@ function drawPromptBoxInitial(inputText) {
|
|
|
646
800
|
tokenDisplay = ` / Tokens: ${color(tokens.toLocaleString())}`;
|
|
647
801
|
}
|
|
648
802
|
|
|
649
|
-
const
|
|
803
|
+
const yoloDisplay = config.yolo ? chalk.bgRed.white.bold(' ⚠️ YOLO ') : '';
|
|
804
|
+
const leftText = ` Provider: ${chalk.cyan(providerDisplay)} / Model: ${chalk.yellow(modelDisplay)} / ${modeDisplay}${tokenDisplay}${yoloDisplay ? ' / ' + yoloDisplay : ''}`;
|
|
650
805
|
const rightText = '/help for shortcuts ';
|
|
651
806
|
|
|
652
807
|
const leftStripped = leftText.replace(/\x1b\[[0-9;]*m/g, '');
|
|
@@ -823,12 +978,35 @@ function promptUser() {
|
|
|
823
978
|
async function main() {
|
|
824
979
|
try {
|
|
825
980
|
config = await loadConfig();
|
|
981
|
+
|
|
982
|
+
if (process.argv.includes('--yolo')) {
|
|
983
|
+
config.yolo = true;
|
|
984
|
+
}
|
|
985
|
+
setYoloMode(config.yolo);
|
|
986
|
+
|
|
826
987
|
await runStartup();
|
|
827
988
|
|
|
828
989
|
if (config.betaTools && config.betaTools.includes('mcp_support')) {
|
|
829
990
|
await mcpManager.init();
|
|
830
991
|
}
|
|
831
992
|
|
|
993
|
+
const apiIdx = process.argv.indexOf('--api');
|
|
994
|
+
if (apiIdx !== -1) {
|
|
995
|
+
const portStr = process.argv[apiIdx + 1];
|
|
996
|
+
const port = portStr && !portStr.startsWith('-') ? parseInt(portStr) : 3000;
|
|
997
|
+
|
|
998
|
+
let host = '127.0.0.1';
|
|
999
|
+
const hostIdx = process.argv.indexOf('--host');
|
|
1000
|
+
if (hostIdx !== -1 && process.argv[hostIdx + 1] && !process.argv[hostIdx + 1].startsWith('-')) {
|
|
1001
|
+
host = process.argv[hostIdx + 1];
|
|
1002
|
+
} else if (process.argv.includes('--expose')) {
|
|
1003
|
+
host = '0.0.0.0';
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
await startApiServer(port, createProvider, host);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
832
1010
|
const resumeIdx = process.argv.indexOf('--resume');
|
|
833
1011
|
if (resumeIdx !== -1) {
|
|
834
1012
|
let resumeId = process.argv[resumeIdx + 1];
|
package/src/permissions.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { select } from '@inquirer/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import crypto from 'crypto';
|
|
3
4
|
|
|
4
5
|
const sessionPermissions = new Set();
|
|
5
6
|
|
|
7
|
+
export function setYoloMode(enabled) {
|
|
8
|
+
global.bananaYoloMode = !!enabled;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
function wrapText(text, width) {
|
|
7
12
|
const lines = [];
|
|
8
13
|
for (let i = 0; i < text.length; i += width) {
|
|
@@ -12,12 +17,25 @@ function wrapText(text, width) {
|
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
export async function requestPermission(actionType, details) {
|
|
20
|
+
if (global.bananaYoloMode || process.argv.includes('--yolo')) {
|
|
21
|
+
return { allowed: true };
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
const permKey = `allow_session_${actionType}`;
|
|
16
25
|
|
|
17
26
|
if (sessionPermissions.has(permKey)) {
|
|
18
27
|
return { allowed: true };
|
|
19
28
|
}
|
|
20
29
|
|
|
30
|
+
if (typeof global.apiPermissionHandler === 'function') {
|
|
31
|
+
const ticketId = crypto.randomUUID();
|
|
32
|
+
const result = await global.apiPermissionHandler(ticketId, actionType, details);
|
|
33
|
+
if (result.remember) {
|
|
34
|
+
sessionPermissions.add(permKey);
|
|
35
|
+
}
|
|
36
|
+
return { allowed: result.allowed };
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
const boxWidth = 41; // Internal width
|
|
22
40
|
|
|
23
41
|
const actionLabel = ` Action: ${actionType}`;
|
package/src/prompt.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
2
4
|
import { getAvailableTools } from './tools/registry.js';
|
|
3
5
|
import { getAvailableSkills } from './utils/skills.js';
|
|
4
6
|
|
|
@@ -29,6 +31,34 @@ SAFETY RULES:
|
|
|
29
31
|
|
|
30
32
|
Always use tools when they would help. Be concise but thorough. `;
|
|
31
33
|
|
|
34
|
+
// Load Project Context (BANANA.md)
|
|
35
|
+
try {
|
|
36
|
+
const bananaPath = path.join(process.cwd(), 'BANANA.md');
|
|
37
|
+
if (fs.existsSync(bananaPath)) {
|
|
38
|
+
const projectContext = fs.readFileSync(bananaPath, 'utf8');
|
|
39
|
+
prompt += `\n\n# Project Context (BANANA.md)\nThe following is the summary of the current project. You already know this; DO NOT use tools to read BANANA.md manually:\n${projectContext}\n`;
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {}
|
|
42
|
+
|
|
43
|
+
// Load Global Memory
|
|
44
|
+
if (config.useMemory !== false) {
|
|
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
|
+
|
|
47
|
+
try {
|
|
48
|
+
const memPath = path.join(os.homedir(), '.config', 'banana-code', 'memory.json');
|
|
49
|
+
if (fs.existsSync(memPath)) {
|
|
50
|
+
const memData = fs.readFileSync(memPath, 'utf8');
|
|
51
|
+
const memories = JSON.parse(memData);
|
|
52
|
+
if (memories.length > 0) {
|
|
53
|
+
prompt += `You have persistently saved the following facts and preferences across ALL projects. Always adhere to these preferences:\n`;
|
|
54
|
+
for (const m of memories) {
|
|
55
|
+
prompt += `- ${m.fact}\n`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {}
|
|
60
|
+
}
|
|
61
|
+
|
|
32
62
|
if (skills && skills.length > 0) {
|
|
33
63
|
prompt += `\n\n# Available Agent Skills\n\nYou have access to the following specialized skills. To activate a skill and receive its detailed instructions, call the \`activate_skill\` tool with the skill's name.\n\n<available_skills>\n`;
|
|
34
64
|
for (const skill of skills) {
|
|
@@ -62,6 +92,26 @@ The user is operating in "Plan Mode".
|
|
|
62
92
|
`;
|
|
63
93
|
}
|
|
64
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
|
+
|
|
65
115
|
if (hasPatchTool) {
|
|
66
116
|
prompt += `
|
|
67
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.`;
|
package/src/providers/claude.js
CHANGED
|
@@ -113,8 +113,14 @@ export class ClaudeProvider {
|
|
|
113
113
|
|
|
114
114
|
const toolResultContent = [];
|
|
115
115
|
for (const call of toolCalls) {
|
|
116
|
+
if (this.config.isApiMode && this.onToolStart) {
|
|
117
|
+
this.onToolStart(call.name);
|
|
118
|
+
}
|
|
116
119
|
console.log(chalk.yellow(`\n[Banana Calling Tool: ${call.name}]`));
|
|
117
120
|
const res = await executeTool(call.name, call.input, this.config);
|
|
121
|
+
if (this.config.isApiMode && this.onToolEnd) {
|
|
122
|
+
this.onToolEnd(res);
|
|
123
|
+
}
|
|
118
124
|
if (this.config.debug) {
|
|
119
125
|
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
120
126
|
}
|
package/src/providers/gemini.js
CHANGED
|
@@ -80,11 +80,16 @@ export class GeminiProvider {
|
|
|
80
80
|
if (content && content.parts) {
|
|
81
81
|
for (const part of content.parts) {
|
|
82
82
|
if (part.text) {
|
|
83
|
-
if (spinner
|
|
83
|
+
if (spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
|
|
84
84
|
if (!this.config.useMarkedTerminal) {
|
|
85
|
-
|
|
85
|
+
if (this.config.isApiMode && this.onChunk) {
|
|
86
|
+
this.onChunk(part.text);
|
|
87
|
+
} else {
|
|
88
|
+
process.stdout.write(chalk.cyan(part.text));
|
|
89
|
+
}
|
|
86
90
|
}
|
|
87
91
|
responseText += part.text;
|
|
92
|
+
|
|
88
93
|
currentTurnText += part.text;
|
|
89
94
|
|
|
90
95
|
// Aggregate sequential text parts
|
|
@@ -134,8 +139,14 @@ export class GeminiProvider {
|
|
|
134
139
|
if (part.functionCall) {
|
|
135
140
|
hasToolCalls = true;
|
|
136
141
|
const call = part.functionCall;
|
|
142
|
+
if (this.config.isApiMode && this.onToolStart) {
|
|
143
|
+
this.onToolStart(call.name);
|
|
144
|
+
}
|
|
137
145
|
console.log(chalk.yellow(`\n[Banana Calling Tool: ${call.name}]`));
|
|
138
146
|
const res = await executeTool(call.name, call.args, this.config);
|
|
147
|
+
if (this.config.isApiMode && this.onToolEnd) {
|
|
148
|
+
this.onToolEnd(res);
|
|
149
|
+
}
|
|
139
150
|
if (this.config.debug) {
|
|
140
151
|
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
141
152
|
}
|
package/src/providers/mistral.js
CHANGED
|
@@ -109,6 +109,9 @@ export class MistralProvider {
|
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
for (const call of toolCalls) {
|
|
112
|
+
if (this.config.isApiMode && this.onToolStart) {
|
|
113
|
+
this.onToolStart(call.function.name);
|
|
114
|
+
}
|
|
112
115
|
console.log(chalk.yellow(`\n[Banana Calling Tool: ${call.function.name}]`));
|
|
113
116
|
let args = {};
|
|
114
117
|
try {
|
|
@@ -116,6 +119,9 @@ export class MistralProvider {
|
|
|
116
119
|
} catch (e) { }
|
|
117
120
|
|
|
118
121
|
const res = await executeTool(call.function.name, args, this.config);
|
|
122
|
+
if (this.config.isApiMode && this.onToolEnd) {
|
|
123
|
+
this.onToolEnd(res);
|
|
124
|
+
}
|
|
119
125
|
if (this.config.debug) {
|
|
120
126
|
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
121
127
|
}
|
package/src/providers/ollama.js
CHANGED
|
@@ -75,9 +75,15 @@ export class OllamaProvider {
|
|
|
75
75
|
|
|
76
76
|
for (const call of messageObj.tool_calls) {
|
|
77
77
|
const fn = call.function;
|
|
78
|
+
if (this.config.isApiMode && this.onToolStart) {
|
|
79
|
+
this.onToolStart(fn.name);
|
|
80
|
+
}
|
|
78
81
|
console.log(chalk.yellow(`\n[Banana Calling Tool: ${fn.name}]`));
|
|
79
82
|
|
|
80
83
|
let res = await executeTool(fn.name, fn.arguments, this.config);
|
|
84
|
+
if (this.config.isApiMode && this.onToolEnd) {
|
|
85
|
+
this.onToolEnd(res);
|
|
86
|
+
}
|
|
81
87
|
if (this.config.debug) {
|
|
82
88
|
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
83
89
|
}
|
|
@@ -84,7 +84,11 @@ export class OllamaCloudProvider {
|
|
|
84
84
|
const content = data.message.content;
|
|
85
85
|
if (spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
|
|
86
86
|
if (!this.config.useMarkedTerminal) {
|
|
87
|
-
|
|
87
|
+
if (this.config.isApiMode && this.onChunk) {
|
|
88
|
+
this.onChunk(content);
|
|
89
|
+
} else {
|
|
90
|
+
process.stdout.write(chalk.cyan(content));
|
|
91
|
+
}
|
|
88
92
|
}
|
|
89
93
|
currentChunkResponse += content;
|
|
90
94
|
finalResponse += content;
|
|
@@ -130,9 +134,15 @@ export class OllamaCloudProvider {
|
|
|
130
134
|
|
|
131
135
|
for (const call of lastMessageObj.tool_calls) {
|
|
132
136
|
const fn = call.function;
|
|
137
|
+
if (this.config.isApiMode && this.onToolStart) {
|
|
138
|
+
this.onToolStart(fn.name);
|
|
139
|
+
}
|
|
133
140
|
console.log(chalk.yellow(`\n[Banana Calling Tool: ${fn.name}]`));
|
|
134
141
|
|
|
135
142
|
let res = await executeTool(fn.name, fn.arguments, this.config);
|
|
143
|
+
if (this.config.isApiMode && this.onToolEnd) {
|
|
144
|
+
this.onToolEnd(res);
|
|
145
|
+
}
|
|
136
146
|
if (this.config.debug) {
|
|
137
147
|
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
138
148
|
}
|
package/src/providers/openai.js
CHANGED
|
@@ -74,7 +74,11 @@ export class OpenAIProvider {
|
|
|
74
74
|
if (delta?.content) {
|
|
75
75
|
if (spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
|
|
76
76
|
if (!this.config.useMarkedTerminal) {
|
|
77
|
-
|
|
77
|
+
if (this.config.isApiMode && this.onChunk) {
|
|
78
|
+
this.onChunk(delta.content);
|
|
79
|
+
} else {
|
|
80
|
+
process.stdout.write(chalk.cyan(delta.content));
|
|
81
|
+
}
|
|
78
82
|
}
|
|
79
83
|
chunkResponse += delta.content;
|
|
80
84
|
finalResponse += delta.content;
|
|
@@ -119,7 +123,10 @@ export class OpenAIProvider {
|
|
|
119
123
|
content: chunkResponse || null
|
|
120
124
|
});
|
|
121
125
|
|
|
122
|
-
for (const call of toolCalls) {
|
|
126
|
+
for (const call of activeToolCalls || toolCalls) {
|
|
127
|
+
if (this.config.isApiMode && this.onToolStart) {
|
|
128
|
+
this.onToolStart(call.function.name);
|
|
129
|
+
}
|
|
123
130
|
console.log(chalk.yellow(`\n[Banana Calling Tool: ${call.function.name}]`));
|
|
124
131
|
let args = {};
|
|
125
132
|
try {
|
|
@@ -127,6 +134,9 @@ export class OpenAIProvider {
|
|
|
127
134
|
} catch (e) { }
|
|
128
135
|
|
|
129
136
|
const res = await executeTool(call.function.name, args, this.config);
|
|
137
|
+
if (this.config.isApiMode && this.onToolEnd) {
|
|
138
|
+
this.onToolEnd(res);
|
|
139
|
+
}
|
|
130
140
|
if (this.config.debug) {
|
|
131
141
|
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
132
142
|
}
|
|
@@ -370,6 +380,9 @@ export class OpenAIProvider {
|
|
|
370
380
|
});
|
|
371
381
|
|
|
372
382
|
for (const call of activeToolCalls) {
|
|
383
|
+
if (this.config.isApiMode && this.onToolStart) {
|
|
384
|
+
this.onToolStart(call.function.name);
|
|
385
|
+
}
|
|
373
386
|
console.log(chalk.yellow(`\n[Banana Calling Tool: ${call.function.name}]`));
|
|
374
387
|
let args = {};
|
|
375
388
|
try {
|
|
@@ -377,6 +390,9 @@ export class OpenAIProvider {
|
|
|
377
390
|
} catch (e) { }
|
|
378
391
|
|
|
379
392
|
const res = await executeTool(call.function.name, args, this.config);
|
|
393
|
+
if (this.config.isApiMode && this.onToolEnd) {
|
|
394
|
+
this.onToolEnd(res);
|
|
395
|
+
}
|
|
380
396
|
if (this.config.debug) {
|
|
381
397
|
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
382
398
|
}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { WebSocketServer } from 'ws';
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { loadConfig } from './config.js';
|
|
7
|
+
import { listSessions, loadSession } from './sessions.js';
|
|
8
|
+
|
|
9
|
+
export async function startApiServer(port = 3000, createProvider, host = '127.0.0.1') {
|
|
10
|
+
const app = express();
|
|
11
|
+
const server = http.createServer(app);
|
|
12
|
+
const wss = new WebSocketServer({ server });
|
|
13
|
+
|
|
14
|
+
app.use(cors());
|
|
15
|
+
app.use(express.json());
|
|
16
|
+
|
|
17
|
+
let config = await loadConfig();
|
|
18
|
+
let providerInstance = null;
|
|
19
|
+
|
|
20
|
+
// WebSocket connection handling
|
|
21
|
+
wss.on('connection', (ws) => {
|
|
22
|
+
console.log(chalk.cyan(`[API] GUI Client connected via WebSocket`));
|
|
23
|
+
|
|
24
|
+
const activeTickets = new Set();
|
|
25
|
+
|
|
26
|
+
// Setup global permission handler for API mode
|
|
27
|
+
global.apiPermissionHandler = (ticketId, actionType, details) => {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const requestPayload = JSON.stringify({
|
|
30
|
+
type: 'permission_requested',
|
|
31
|
+
ticketId,
|
|
32
|
+
action: actionType,
|
|
33
|
+
details
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (ws.readyState === ws.OPEN) {
|
|
37
|
+
activeTickets.add(ticketId);
|
|
38
|
+
ws.send(requestPayload);
|
|
39
|
+
console.log(chalk.gray(`[API] Sent permission request: ${ticketId}`));
|
|
40
|
+
} else {
|
|
41
|
+
console.log(chalk.red(`[API] WebSocket closed, denying permission automatically.`));
|
|
42
|
+
resolve({ allowed: false });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Temporary listener to catch the GUI's response for this specific ticket
|
|
47
|
+
const responseHandler = (message) => {
|
|
48
|
+
try {
|
|
49
|
+
const data = JSON.parse(message);
|
|
50
|
+
if (data.type === 'permission_response' && data.ticketId === ticketId) {
|
|
51
|
+
console.log(chalk.gray(`[API] Received permission response for ${ticketId}: ${data.allowed}`));
|
|
52
|
+
activeTickets.delete(ticketId);
|
|
53
|
+
ws.removeListener('message', responseHandler); // clean up
|
|
54
|
+
resolve({ allowed: data.allowed, remember: data.session });
|
|
55
|
+
}
|
|
56
|
+
} catch (e) {}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
ws.on('message', responseHandler);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
ws.on('message', async (message) => {
|
|
64
|
+
console.log(chalk.gray(`[API] Received message: ${message}`));
|
|
65
|
+
try {
|
|
66
|
+
const data = JSON.parse(message);
|
|
67
|
+
|
|
68
|
+
// Ignore valid permission responses here, they are handled by the specific ticket listeners
|
|
69
|
+
if (data.type === 'permission_response') {
|
|
70
|
+
if (!activeTickets.has(data.ticketId)) {
|
|
71
|
+
console.log(chalk.red(`[API] Invalid ticket ID received: ${data.ticketId}`));
|
|
72
|
+
if (ws.readyState === ws.OPEN) {
|
|
73
|
+
ws.send(JSON.stringify({
|
|
74
|
+
type: 'error',
|
|
75
|
+
message: `Permission Denied: Ticket ID '${data.ticketId}' does not match any active requests.`
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (data.type === 'chat') {
|
|
83
|
+
if (!providerInstance) {
|
|
84
|
+
console.log(chalk.gray(`[API] Creating provider instance...`));
|
|
85
|
+
providerInstance = createProvider(config);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Attach a temporary listener for this specific request
|
|
89
|
+
// We modify the provider config locally for this mode
|
|
90
|
+
providerInstance.config.isApiMode = true;
|
|
91
|
+
providerInstance.onChunk = (chunk) => {
|
|
92
|
+
if (ws.readyState === ws.OPEN) {
|
|
93
|
+
ws.send(JSON.stringify({ type: 'chunk', content: chunk }));
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
providerInstance.onToolStart = (tool) => {
|
|
97
|
+
if (ws.readyState === ws.OPEN) {
|
|
98
|
+
ws.send(JSON.stringify({ type: 'tool_start', tool }));
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
providerInstance.onToolEnd = (result) => {
|
|
102
|
+
if (ws.readyState === ws.OPEN) {
|
|
103
|
+
ws.send(JSON.stringify({ type: 'tool_end', result }));
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
console.log(chalk.gray(`[API] Sending message to AI...`));
|
|
108
|
+
const response = await providerInstance.sendMessage(data.text);
|
|
109
|
+
console.log(chalk.gray(`[API] AI response complete.`));
|
|
110
|
+
|
|
111
|
+
if (ws.readyState === ws.OPEN) {
|
|
112
|
+
ws.send(JSON.stringify({ type: 'done', finalResponse: response }));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error(chalk.red(`[API] Error: ${err.message}`));
|
|
117
|
+
if (ws.readyState === ws.OPEN) {
|
|
118
|
+
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
ws.on('close', () => {
|
|
124
|
+
console.log(chalk.gray(`[API] GUI Client disconnected`));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// HTTP Endpoints
|
|
130
|
+
app.get('/api/sessions', async (req, res) => {
|
|
131
|
+
const sessions = await listSessions();
|
|
132
|
+
res.json(sessions);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
app.get('/api/config', (req, res) => {
|
|
136
|
+
res.json(config);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
app.get('/api/status', (req, res) => {
|
|
140
|
+
res.json({ status: 'running', provider: config.provider, model: config.model });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
server.listen(port, host, () => {
|
|
144
|
+
console.log(chalk.green.bold(`\n🍌 Banana Code API Server running at http://${host}:${port}`));
|
|
145
|
+
console.log(chalk.gray(`WebSocket streaming enabled on the same port.\n`));
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { addMemory, removeMemory, loadMemory } from '../utils/memory.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
|
|
5
|
+
export async function saveMemoryTool({ fact }) {
|
|
6
|
+
const spinner = ora({ text: `Saving memory...`, color: 'magenta', stream: process.stdout }).start();
|
|
7
|
+
try {
|
|
8
|
+
const id = await addMemory(fact);
|
|
9
|
+
spinner.stop();
|
|
10
|
+
return `Successfully saved fact. ID: ${id}`;
|
|
11
|
+
} catch (err) {
|
|
12
|
+
spinner.stop();
|
|
13
|
+
return `Error saving memory: ${err.message}`;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function listMemoryTool() {
|
|
18
|
+
const spinner = ora({ text: `Reading memories...`, color: 'magenta', stream: process.stdout }).start();
|
|
19
|
+
try {
|
|
20
|
+
const memories = await loadMemory();
|
|
21
|
+
spinner.stop();
|
|
22
|
+
if (memories.length === 0) {
|
|
23
|
+
return `No memories currently saved.`;
|
|
24
|
+
}
|
|
25
|
+
return JSON.stringify(memories, null, 2);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
spinner.stop();
|
|
28
|
+
return `Error listing memories: ${err.message}`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function deleteMemoryTool({ id }) {
|
|
33
|
+
const spinner = ora({ text: `Deleting memory ${id}...`, color: 'magenta', stream: process.stdout }).start();
|
|
34
|
+
try {
|
|
35
|
+
const success = await removeMemory(id);
|
|
36
|
+
spinner.stop();
|
|
37
|
+
if (success) {
|
|
38
|
+
return `Successfully deleted memory with ID: ${id}`;
|
|
39
|
+
} else {
|
|
40
|
+
return `Error: Memory with ID '${id}' not found.`;
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
spinner.stop();
|
|
44
|
+
return `Error deleting memory: ${err.message}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -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';
|
|
@@ -10,6 +11,7 @@ import { patchFile } from './patchFile.js';
|
|
|
10
11
|
import { activateSkill } from './activateSkill.js';
|
|
11
12
|
import { delegateTask } from './delegateTask.js';
|
|
12
13
|
import { mcpManager } from '../utils/mcp.js';
|
|
14
|
+
import { saveMemoryTool, listMemoryTool, deleteMemoryTool } from './memoryTools.js';
|
|
13
15
|
|
|
14
16
|
export const TOOLS = [
|
|
15
17
|
{
|
|
@@ -35,6 +37,21 @@ export const TOOLS = [
|
|
|
35
37
|
required: ['filepath']
|
|
36
38
|
}
|
|
37
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
|
+
},
|
|
38
55
|
{
|
|
39
56
|
name: 'write_file',
|
|
40
57
|
description: 'Write content to a file. Overwrites existing content.',
|
|
@@ -109,9 +126,9 @@ export const TOOLS = [
|
|
|
109
126
|
},
|
|
110
127
|
{
|
|
111
128
|
name: 'patch_file',
|
|
112
|
-
label: 'Surgical File Patch
|
|
129
|
+
label: 'Surgical File Patch',
|
|
113
130
|
description: 'Edit a file by replacing specific sections of text. Much more efficient for large files.',
|
|
114
|
-
|
|
131
|
+
settingsFeature: 'usePatchFile',
|
|
115
132
|
parameters: {
|
|
116
133
|
type: 'object',
|
|
117
134
|
properties: {
|
|
@@ -170,14 +187,59 @@ export const TOOLS = [
|
|
|
170
187
|
},
|
|
171
188
|
required: ['task']
|
|
172
189
|
}
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'save_memory',
|
|
193
|
+
description: 'Persists a fact across ALL future sessions globally. Use this ONLY to save facts or preferences you want to permanently remember across different projects. Do NOT use for session-specific or temporary data.',
|
|
194
|
+
memoryFeature: true,
|
|
195
|
+
parameters: {
|
|
196
|
+
type: 'object',
|
|
197
|
+
properties: {
|
|
198
|
+
fact: { type: 'string', description: 'A concise fact or preference to remember globally.' }
|
|
199
|
+
},
|
|
200
|
+
required: ['fact']
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: 'list_memory',
|
|
205
|
+
description: 'Retrieves all globally saved memories with their IDs. Use this to review facts you have saved or find an ID to delete an outdated fact.',
|
|
206
|
+
memoryFeature: true,
|
|
207
|
+
parameters: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {},
|
|
210
|
+
required: []
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'delete_memory',
|
|
215
|
+
description: 'Deletes a specific global memory using its ID. Call list_memory first to find the ID.',
|
|
216
|
+
memoryFeature: true,
|
|
217
|
+
parameters: {
|
|
218
|
+
type: 'object',
|
|
219
|
+
properties: {
|
|
220
|
+
id: { type: 'string', description: 'The exact ID of the memory to delete.' }
|
|
221
|
+
},
|
|
222
|
+
required: ['id']
|
|
223
|
+
}
|
|
173
224
|
}
|
|
174
225
|
];
|
|
175
226
|
|
|
176
227
|
export function getAvailableTools(config = {}) {
|
|
177
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
|
+
}
|
|
178
233
|
if (tool.beta) {
|
|
179
234
|
return config.betaTools && config.betaTools.includes(tool.name);
|
|
180
235
|
}
|
|
236
|
+
if (tool.memoryFeature) {
|
|
237
|
+
return config.useMemory !== false;
|
|
238
|
+
}
|
|
239
|
+
if (tool.settingsFeature) {
|
|
240
|
+
// Default to true if not explicitly set to false
|
|
241
|
+
return config[tool.settingsFeature] !== false;
|
|
242
|
+
}
|
|
181
243
|
return true;
|
|
182
244
|
});
|
|
183
245
|
|
|
@@ -232,6 +294,7 @@ export async function executeTool(name, args, config) {
|
|
|
232
294
|
switch (name) {
|
|
233
295
|
case 'execute_command': return await execCommand(args);
|
|
234
296
|
case 'read_file': return await readFile(args);
|
|
297
|
+
case 'read_many_files': return await readManyFiles(args);
|
|
235
298
|
case 'write_file': return await writeFile(args);
|
|
236
299
|
case 'fetch_url': return await fetchUrl(args);
|
|
237
300
|
case 'search_files': return await searchFiles(args);
|
|
@@ -241,6 +304,9 @@ export async function executeTool(name, args, config) {
|
|
|
241
304
|
case 'patch_file': return await patchFile(args);
|
|
242
305
|
case 'activate_skill': return await activateSkill(args);
|
|
243
306
|
case 'delegate_task': return await delegateTask(args, config);
|
|
307
|
+
case 'save_memory': return await saveMemoryTool(args);
|
|
308
|
+
case 'list_memory': return await listMemoryTool(args);
|
|
309
|
+
case 'delete_memory': return await deleteMemoryTool(args);
|
|
244
310
|
default: return `Unknown tool: ${name}`;
|
|
245
311
|
}
|
|
246
312
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
|
|
6
|
+
const MEMORY_DIR = path.join(os.homedir(), '.config', 'banana-code');
|
|
7
|
+
const MEMORY_FILE = path.join(MEMORY_DIR, 'memory.json');
|
|
8
|
+
|
|
9
|
+
export async function loadMemory() {
|
|
10
|
+
try {
|
|
11
|
+
const data = await fs.readFile(MEMORY_FILE, 'utf-8');
|
|
12
|
+
return JSON.parse(data);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (error.code === 'ENOENT') {
|
|
15
|
+
return []; // Return empty array if file doesn't exist
|
|
16
|
+
}
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function saveMemory(memories) {
|
|
22
|
+
try {
|
|
23
|
+
await fs.mkdir(MEMORY_DIR, { recursive: true });
|
|
24
|
+
await fs.writeFile(MEMORY_FILE, JSON.stringify(memories, null, 2), 'utf-8');
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error("Failed to save memory:", error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function addMemory(fact) {
|
|
31
|
+
const memories = await loadMemory();
|
|
32
|
+
const newMemory = {
|
|
33
|
+
id: crypto.randomUUID().slice(0, 8),
|
|
34
|
+
fact,
|
|
35
|
+
createdAt: new Date().toISOString()
|
|
36
|
+
};
|
|
37
|
+
memories.push(newMemory);
|
|
38
|
+
await saveMemory(memories);
|
|
39
|
+
return newMemory.id;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function removeMemory(id) {
|
|
43
|
+
const memories = await loadMemory();
|
|
44
|
+
const initialLength = memories.length;
|
|
45
|
+
const filtered = memories.filter(m => m.id !== id);
|
|
46
|
+
|
|
47
|
+
if (filtered.length < initialLength) {
|
|
48
|
+
await saveMemory(filtered);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|