@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 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.5.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
- const modeDisplay = config.planMode ? chalk.magenta('PLAN MODE') : chalk.green('AGENT MODE');
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 leftText = ` Provider: ${chalk.cyan(providerDisplay)} / Model: ${chalk.yellow(modelDisplay)} / ${modeDisplay}${tokenDisplay}`;
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
- const modeDisplay = config.planMode ? chalk.magenta('PLAN MODE') : chalk.green('AGENT MODE');
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 leftText = ` Provider: ${chalk.cyan(providerDisplay)} / Model: ${chalk.yellow(modelDisplay)} / ${modeDisplay}${tokenDisplay}`;
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];
@@ -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.`;
@@ -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
  }
@@ -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 && spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
83
+ if (spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
84
84
  if (!this.config.useMarkedTerminal) {
85
- process.stdout.write(chalk.cyan(part.text));
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
  }
@@ -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
  }
@@ -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
- process.stdout.write(chalk.cyan(content));
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
  }
@@ -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
- process.stdout.write(chalk.cyan(delta.content));
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
+ }
@@ -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 (Beta)',
129
+ label: 'Surgical File Patch',
113
130
  description: 'Edit a file by replacing specific sections of text. Much more efficient for large files.',
114
- beta: true,
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
+ }