@banaxi/banana-code 1.5.1 → 1.6.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/index.js +108 -0
- package/src/permissions.js +10 -0
- package/src/prompt.js +30 -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/registry.js +47 -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.6.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/index.js
CHANGED
|
@@ -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;
|
|
@@ -424,6 +437,91 @@ async function handleSlashCommand(command) {
|
|
|
424
437
|
}
|
|
425
438
|
}
|
|
426
439
|
break;
|
|
440
|
+
case '/memory':
|
|
441
|
+
if (!config.useMemory) {
|
|
442
|
+
console.log(chalk.yellow("Global AI Memory is disabled. Enable it in /settings first."));
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
const { select: memSelect, input: memInput } = await import('@inquirer/prompts');
|
|
446
|
+
const { loadMemory, removeMemory, addMemory } = await import('./utils/memory.js');
|
|
447
|
+
let memAction = await memSelect({
|
|
448
|
+
message: 'Manage Global AI Memory:',
|
|
449
|
+
choices: [
|
|
450
|
+
{ name: 'View all memories', value: 'view' },
|
|
451
|
+
{ name: 'Add a new memory manually', value: 'add' },
|
|
452
|
+
{ name: 'Delete a memory', value: 'delete' }
|
|
453
|
+
],
|
|
454
|
+
loop: false
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
if (memAction === 'view') {
|
|
458
|
+
const mems = await loadMemory();
|
|
459
|
+
if (mems.length === 0) {
|
|
460
|
+
console.log(chalk.yellow("No global memories saved yet."));
|
|
461
|
+
} else {
|
|
462
|
+
console.log(chalk.cyan.bold("\nGlobal Memories:"));
|
|
463
|
+
mems.forEach(m => {
|
|
464
|
+
console.log(chalk.gray(`[${m.id}] `) + chalk.green(m.fact));
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
} else if (memAction === 'add') {
|
|
468
|
+
const newFact = await memInput({
|
|
469
|
+
message: 'Enter the fact you want the AI to remember globally:',
|
|
470
|
+
validate: (v) => v.trim().length > 0 || 'Memory cannot be empty'
|
|
471
|
+
});
|
|
472
|
+
const id = await addMemory(newFact);
|
|
473
|
+
console.log(chalk.green(`Memory saved with ID: ${id}`));
|
|
474
|
+
providerInstance = createProvider(); // Reload provider to inject new memory
|
|
475
|
+
} else if (memAction === 'delete') {
|
|
476
|
+
const mems = await loadMemory();
|
|
477
|
+
if (mems.length === 0) {
|
|
478
|
+
console.log(chalk.yellow("No memories to delete."));
|
|
479
|
+
} else {
|
|
480
|
+
const idToDelete = await memSelect({
|
|
481
|
+
message: 'Select a memory to delete:',
|
|
482
|
+
choices: mems.map(m => ({ name: m.fact, value: m.id })),
|
|
483
|
+
loop: false,
|
|
484
|
+
pageSize: 10
|
|
485
|
+
});
|
|
486
|
+
const success = await removeMemory(idToDelete);
|
|
487
|
+
if (success) {
|
|
488
|
+
console.log(chalk.green(`Memory deleted.`));
|
|
489
|
+
providerInstance = createProvider(); // Reload provider
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
break;
|
|
494
|
+
case '/init':
|
|
495
|
+
console.log(chalk.cyan("Generating project summary for BANANA.md..."));
|
|
496
|
+
const initSpinner = ora({ text: 'Analyzing project...', color: 'yellow', stream: process.stdout }).start();
|
|
497
|
+
try {
|
|
498
|
+
const { getWorkspaceTree } = await import('./utils/workspace.js');
|
|
499
|
+
const tree = await getWorkspaceTree();
|
|
500
|
+
|
|
501
|
+
const initProvider = createProvider();
|
|
502
|
+
// We use a completely blank slate for this so it doesn't get confused
|
|
503
|
+
initProvider.messages = [];
|
|
504
|
+
|
|
505
|
+
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.";
|
|
506
|
+
initPrompt += `\n\n--- Project Tree ---\n${tree}`;
|
|
507
|
+
|
|
508
|
+
const summary = await initProvider.sendMessage(initPrompt);
|
|
509
|
+
|
|
510
|
+
const fs = await import('fs/promises');
|
|
511
|
+
const path = await import('path');
|
|
512
|
+
const bananaPath = path.join(process.cwd(), 'BANANA.md');
|
|
513
|
+
await fs.writeFile(bananaPath, summary, 'utf8');
|
|
514
|
+
|
|
515
|
+
initSpinner.stop();
|
|
516
|
+
console.log(chalk.green(`Successfully created BANANA.md!`));
|
|
517
|
+
|
|
518
|
+
// Re-init current provider so it picks up the new BANANA.md
|
|
519
|
+
providerInstance = createProvider();
|
|
520
|
+
} catch (err) {
|
|
521
|
+
initSpinner.stop();
|
|
522
|
+
console.log(chalk.red(`Failed to initialize project: ${err.message}`));
|
|
523
|
+
}
|
|
524
|
+
break;
|
|
427
525
|
case '/help':
|
|
428
526
|
console.log(chalk.yellow(`
|
|
429
527
|
Available commands:
|
|
@@ -437,6 +535,8 @@ Available commands:
|
|
|
437
535
|
/beta - Manage beta features and tools
|
|
438
536
|
/settings - Manage app settings (workspace auto-feed, etc)
|
|
439
537
|
/skills - List loaded agent skills
|
|
538
|
+
/memory - Manage global AI memories
|
|
539
|
+
/init - Generate a BANANA.md project summary file
|
|
440
540
|
/plan - Enable Plan Mode (AI proposes a plan for big changes)
|
|
441
541
|
/agent - Enable Agent Mode (default, AI edits directly)
|
|
442
542
|
/debug - Toggle debug mode (show tool results)
|
|
@@ -829,6 +929,14 @@ async function main() {
|
|
|
829
929
|
await mcpManager.init();
|
|
830
930
|
}
|
|
831
931
|
|
|
932
|
+
const apiIdx = process.argv.indexOf('--api');
|
|
933
|
+
if (apiIdx !== -1) {
|
|
934
|
+
const portStr = process.argv[apiIdx + 1];
|
|
935
|
+
const port = portStr && !portStr.startsWith('-') ? parseInt(portStr) : 3000;
|
|
936
|
+
await startApiServer(port, createProvider);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
832
940
|
const resumeIdx = process.argv.indexOf('--resume');
|
|
833
941
|
if (resumeIdx !== -1) {
|
|
834
942
|
let resumeId = process.argv[resumeIdx + 1];
|
package/src/permissions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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
|
|
|
@@ -18,6 +19,15 @@ export async function requestPermission(actionType, details) {
|
|
|
18
19
|
return { allowed: true };
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
if (typeof global.apiPermissionHandler === 'function') {
|
|
23
|
+
const ticketId = crypto.randomUUID();
|
|
24
|
+
const result = await global.apiPermissionHandler(ticketId, actionType, details);
|
|
25
|
+
if (result.remember) {
|
|
26
|
+
sessionPermissions.add(permKey);
|
|
27
|
+
}
|
|
28
|
+
return { allowed: result.allowed };
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
const boxWidth = 41; // Internal width
|
|
22
32
|
|
|
23
33
|
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) {
|
|
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) {
|
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) {
|
|
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, () => {
|
|
144
|
+
console.log(chalk.green.bold(`\n🍌 Banana Code API Server running at http://localhost:${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
|
+
}
|
package/src/tools/registry.js
CHANGED
|
@@ -10,6 +10,7 @@ import { patchFile } from './patchFile.js';
|
|
|
10
10
|
import { activateSkill } from './activateSkill.js';
|
|
11
11
|
import { delegateTask } from './delegateTask.js';
|
|
12
12
|
import { mcpManager } from '../utils/mcp.js';
|
|
13
|
+
import { saveMemoryTool, listMemoryTool, deleteMemoryTool } from './memoryTools.js';
|
|
13
14
|
|
|
14
15
|
export const TOOLS = [
|
|
15
16
|
{
|
|
@@ -109,9 +110,9 @@ export const TOOLS = [
|
|
|
109
110
|
},
|
|
110
111
|
{
|
|
111
112
|
name: 'patch_file',
|
|
112
|
-
label: 'Surgical File Patch
|
|
113
|
+
label: 'Surgical File Patch',
|
|
113
114
|
description: 'Edit a file by replacing specific sections of text. Much more efficient for large files.',
|
|
114
|
-
|
|
115
|
+
settingsFeature: 'usePatchFile',
|
|
115
116
|
parameters: {
|
|
116
117
|
type: 'object',
|
|
117
118
|
properties: {
|
|
@@ -170,6 +171,40 @@ export const TOOLS = [
|
|
|
170
171
|
},
|
|
171
172
|
required: ['task']
|
|
172
173
|
}
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'save_memory',
|
|
177
|
+
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.',
|
|
178
|
+
memoryFeature: true,
|
|
179
|
+
parameters: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties: {
|
|
182
|
+
fact: { type: 'string', description: 'A concise fact or preference to remember globally.' }
|
|
183
|
+
},
|
|
184
|
+
required: ['fact']
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'list_memory',
|
|
189
|
+
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.',
|
|
190
|
+
memoryFeature: true,
|
|
191
|
+
parameters: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {},
|
|
194
|
+
required: []
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'delete_memory',
|
|
199
|
+
description: 'Deletes a specific global memory using its ID. Call list_memory first to find the ID.',
|
|
200
|
+
memoryFeature: true,
|
|
201
|
+
parameters: {
|
|
202
|
+
type: 'object',
|
|
203
|
+
properties: {
|
|
204
|
+
id: { type: 'string', description: 'The exact ID of the memory to delete.' }
|
|
205
|
+
},
|
|
206
|
+
required: ['id']
|
|
207
|
+
}
|
|
173
208
|
}
|
|
174
209
|
];
|
|
175
210
|
|
|
@@ -178,6 +213,13 @@ export function getAvailableTools(config = {}) {
|
|
|
178
213
|
if (tool.beta) {
|
|
179
214
|
return config.betaTools && config.betaTools.includes(tool.name);
|
|
180
215
|
}
|
|
216
|
+
if (tool.memoryFeature) {
|
|
217
|
+
return config.useMemory === true;
|
|
218
|
+
}
|
|
219
|
+
if (tool.settingsFeature) {
|
|
220
|
+
// Default to true if not explicitly set to false
|
|
221
|
+
return config[tool.settingsFeature] !== false;
|
|
222
|
+
}
|
|
181
223
|
return true;
|
|
182
224
|
});
|
|
183
225
|
|
|
@@ -241,6 +283,9 @@ export async function executeTool(name, args, config) {
|
|
|
241
283
|
case 'patch_file': return await patchFile(args);
|
|
242
284
|
case 'activate_skill': return await activateSkill(args);
|
|
243
285
|
case 'delegate_task': return await delegateTask(args, config);
|
|
286
|
+
case 'save_memory': return await saveMemoryTool(args);
|
|
287
|
+
case 'list_memory': return await listMemoryTool(args);
|
|
288
|
+
case 'delete_memory': return await deleteMemoryTool(args);
|
|
244
289
|
default: return `Unknown tool: ${name}`;
|
|
245
290
|
}
|
|
246
291
|
}
|
|
@@ -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
|
+
}
|