@banaxi/banana-code 1.0.5 → 1.2.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/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import readline from 'readline';
2
2
  import chalk from 'chalk';
3
+ import ora from 'ora';
3
4
  import { loadConfig, saveConfig, setupProvider } from './config.js';
4
5
  import { runStartup } from './startup.js';
5
6
  import { getSessionPermissions } from './permissions.js';
@@ -8,12 +9,18 @@ import { GeminiProvider } from './providers/gemini.js';
8
9
  import { ClaudeProvider } from './providers/claude.js';
9
10
  import { OpenAIProvider } from './providers/openai.js';
10
11
  import { OllamaProvider } from './providers/ollama.js';
12
+ import { OllamaCloudProvider } from './providers/ollamaCloud.js';
11
13
 
12
14
  import { loadSession, saveSession, generateSessionId, getLatestSessionId, listSessions } from './sessions.js';
15
+ import { printMarkdown } from './utils/markdown.js';
16
+ import { estimateConversationTokens } from './utils/tokens.js';
13
17
 
14
18
  let config;
15
19
  let providerInstance;
16
20
  let currentSessionId;
21
+ const commandHistory = [];
22
+ let historyIndex = -1;
23
+ let currentInputSaved = '';
17
24
 
18
25
  function createProvider(overrideConfig = null) {
19
26
  const activeConfig = overrideConfig || config;
@@ -21,6 +28,7 @@ function createProvider(overrideConfig = null) {
21
28
  case 'gemini': return new GeminiProvider(activeConfig);
22
29
  case 'claude': return new ClaudeProvider(activeConfig);
23
30
  case 'openai': return new OpenAIProvider(activeConfig);
31
+ case 'ollama_cloud': return new OllamaCloudProvider(activeConfig);
24
32
  case 'ollama': return new OllamaProvider(activeConfig);
25
33
  default:
26
34
  console.log(chalk.red(`Unknown provider: ${activeConfig.provider}. Defaulting to Ollama.`));
@@ -43,19 +51,20 @@ async function handleSlashCommand(command) {
43
51
  { name: 'Google Gemini', value: 'gemini' },
44
52
  { name: 'Anthropic Claude', value: 'claude' },
45
53
  { name: 'OpenAI', value: 'openai' },
54
+ { name: 'Ollama Cloud', value: 'ollama_cloud' },
46
55
  { name: 'Ollama (Local)', value: 'ollama' }
47
56
  ]
48
57
  });
49
58
  }
50
59
 
51
- if (['gemini', 'claude', 'openai', 'ollama'].includes(newProv)) {
60
+ if (['gemini', 'claude', 'openai', 'ollama_cloud', 'ollama'].includes(newProv)) {
52
61
  // Use the shared setup logic to get keys/models
53
62
  config = await setupProvider(newProv, config);
54
63
  await saveConfig(config);
55
64
  providerInstance = createProvider();
56
65
  console.log(chalk.green(`Switched provider to ${newProv} (${config.model}).`));
57
66
  } else {
58
- console.log(chalk.yellow(`Usage: /provider <gemini|claude|openai|ollama>`));
67
+ console.log(chalk.yellow(`Usage: /provider <gemini|claude|openai|ollama_cloud|ollama>`));
59
68
  }
60
69
  break;
61
70
  case '/model':
@@ -63,13 +72,15 @@ async function handleSlashCommand(command) {
63
72
  if (!newModel) {
64
73
  // Interactive selection
65
74
  const { select } = await import('@inquirer/prompts');
66
- const { GEMINI_MODELS, CLAUDE_MODELS, OPENAI_MODELS, CODEX_MODELS } = await import('./constants.js');
75
+ const { GEMINI_MODELS, CLAUDE_MODELS, OPENAI_MODELS, CODEX_MODELS, OLLAMA_CLOUD_MODELS } = await import('./constants.js');
67
76
 
68
77
  let choices = [];
69
78
  if (config.provider === 'gemini') choices = GEMINI_MODELS;
70
79
  else if (config.provider === 'claude') choices = CLAUDE_MODELS;
71
80
  else if (config.provider === 'openai') {
72
81
  choices = config.authType === 'oauth' ? CODEX_MODELS : OPENAI_MODELS;
82
+ } else if (config.provider === 'ollama_cloud') {
83
+ choices = OLLAMA_CLOUD_MODELS;
73
84
  } else if (config.provider === 'ollama') {
74
85
  try {
75
86
  const response = await fetch('http://localhost:11434/api/tags');
@@ -82,10 +93,25 @@ async function handleSlashCommand(command) {
82
93
  }
83
94
 
84
95
  if (choices.length > 0) {
96
+ const finalChoices = [...choices];
97
+ if (config.provider === 'ollama_cloud') {
98
+ finalChoices.push({ name: chalk.magenta('✎ Enter custom model ID...'), value: 'CUSTOM_ID' });
99
+ }
100
+
85
101
  newModel = await select({
86
102
  message: 'Select a model:',
87
- choices
103
+ choices: finalChoices,
104
+ loop: false,
105
+ pageSize: Math.max(finalChoices.length, 15)
88
106
  });
107
+
108
+ if (newModel === 'CUSTOM_ID') {
109
+ const { input } = await import('@inquirer/prompts');
110
+ newModel = await input({
111
+ message: 'Enter the exact model ID (e.g., gemma3:27b-cloud):',
112
+ validate: (v) => v.trim().length > 0 || 'Model ID cannot be empty'
113
+ });
114
+ }
89
115
  }
90
116
  }
91
117
 
@@ -106,11 +132,86 @@ async function handleSlashCommand(command) {
106
132
  providerInstance = createProvider(); // fresh instance = clear history
107
133
  console.log(chalk.green('Chat history cleared.'));
108
134
  break;
135
+ case '/clean':
136
+ if (!config.betaTools || !config.betaTools.includes('clean_command')) {
137
+ console.log(chalk.yellow("The /clean command is a beta feature. You need to enable it in the /beta menu first."));
138
+ break;
139
+ }
140
+ const msgCount = providerInstance.messages ? providerInstance.messages.length : 0;
141
+ if (msgCount <= 2) {
142
+ console.log(chalk.yellow("Not enough history to summarize."));
143
+ break;
144
+ }
145
+
146
+ console.log(chalk.cyan("Summarizing context to save tokens..."));
147
+ const summarySpinner = ora({ text: 'Compressing history...', color: 'yellow', stream: process.stdout }).start();
148
+
149
+ try {
150
+ // Temporarily disable terminal formatting for the summary request
151
+ const originalUseMarked = config.useMarkedTerminal;
152
+ config.useMarkedTerminal = false;
153
+
154
+ // Create a temporary prompt asking for a summary
155
+ const summaryPrompt = "SYSTEM INSTRUCTION: Please provide a highly concise summary of our entire conversation so far. Focus ONLY on the overall goal, the current state of the project, any important decisions made, and what we were about to do next. Do not include pleasantries. This summary will be used as your memory going forward.";
156
+
157
+ // Ask the AI to summarize
158
+ const summary = await providerInstance.sendMessage(summaryPrompt);
159
+
160
+ // Restore settings
161
+ config.useMarkedTerminal = originalUseMarked;
162
+ summarySpinner.stop();
163
+
164
+ // Re-initialize the provider to wipe old history
165
+ providerInstance = createProvider();
166
+
167
+ // Inject the summary as the first message after the system prompt
168
+ const summaryMemory = `[PREVIOUS CONVERSATION SUMMARY]\n${summary}`;
169
+
170
+ if (config.provider === 'gemini') {
171
+ providerInstance.messages.push({ role: 'user', parts: [{ text: summaryMemory }] });
172
+ providerInstance.messages.push({ role: 'model', parts: [{ text: "I have stored the summary of our previous conversation in my memory." }] });
173
+ } else if (config.provider === 'claude') {
174
+ providerInstance.messages.push({ role: 'user', content: summaryMemory });
175
+ providerInstance.messages.push({ role: 'assistant', content: "I have stored the summary of our previous conversation in my memory." });
176
+ } else {
177
+ providerInstance.messages.push({ role: 'user', content: summaryMemory });
178
+ providerInstance.messages.push({ role: 'assistant', content: "I have stored the summary of our previous conversation in my memory." });
179
+ }
180
+
181
+ console.log(chalk.green(`\nContext successfully compressed!`));
182
+ if (config.debug) {
183
+ console.log(chalk.gray(`\n[Saved Summary]:\n${summary}\n`));
184
+ }
185
+
186
+ await saveSession(currentSessionId, {
187
+ provider: config.provider,
188
+ model: config.model || providerInstance.modelName,
189
+ messages: providerInstance.messages
190
+ });
191
+
192
+ } catch (err) {
193
+ summarySpinner.stop();
194
+ console.log(chalk.red(`Failed to compress context: ${err.message}`));
195
+ }
196
+ break;
109
197
  case '/context':
110
198
  let length = 0;
111
- if (providerInstance.messages) length = providerInstance.messages.length;
112
- else if (providerInstance.chat) length = (await providerInstance.chat.getHistory()).length;
113
- console.log(chalk.cyan(`Current context contains approximately ${length} messages.`));
199
+ let messagesForEstimation = [];
200
+
201
+ if (providerInstance.messages) {
202
+ length = providerInstance.messages.length;
203
+ messagesForEstimation = providerInstance.messages;
204
+ } else if (providerInstance.chat) {
205
+ messagesForEstimation = await providerInstance.chat.getHistory();
206
+ length = messagesForEstimation.length;
207
+ }
208
+
209
+ const { estimateConversationTokens } = await import('./utils/tokens.js');
210
+ const estimatedTokens = estimateConversationTokens(messagesForEstimation);
211
+
212
+ console.log(chalk.cyan(`Current context:`));
213
+ console.log(chalk.cyan(`- Messages: ${length}`));
214
+ console.log(chalk.cyan(`- Estimated Tokens: ~${estimatedTokens.toLocaleString()}`));
114
215
  break;
115
216
  case '/permissions':
116
217
  const perms = getSessionPermissions();
@@ -125,18 +226,27 @@ async function handleSlashCommand(command) {
125
226
  const { TOOLS } = await import('./tools/registry.js');
126
227
  const betaTools = TOOLS.filter(t => t.beta);
127
228
 
128
- if (betaTools.length === 0) {
129
- console.log(chalk.yellow("No beta tools available."));
229
+ let choices = betaTools.map(t => ({
230
+ name: t.label || t.name,
231
+ value: t.name,
232
+ checked: (config.betaTools || []).includes(t.name)
233
+ }));
234
+
235
+ // Add beta commands that aren't tools
236
+ choices.push({
237
+ name: '/clean command (Context Compression)',
238
+ value: 'clean_command',
239
+ checked: (config.betaTools || []).includes('clean_command')
240
+ });
241
+
242
+ if (choices.length === 0) {
243
+ console.log(chalk.yellow("No beta features available."));
130
244
  break;
131
245
  }
132
246
 
133
247
  const enabledBetaTools = await checkbox({
134
- message: 'Select beta tools to activate (Space to toggle, Enter to confirm):',
135
- choices: betaTools.map(t => ({
136
- name: t.label || t.name,
137
- value: t.name,
138
- checked: (config.betaTools || []).includes(t.name)
139
- }))
248
+ message: 'Select beta features to activate (Space to toggle, Enter to confirm):',
249
+ choices: choices
140
250
  });
141
251
 
142
252
  if (enabledBetaTools.includes('duck_duck_go_scrape') && !(config.betaTools || []).includes('duck_duck_go_scrape')) {
@@ -158,15 +268,103 @@ async function handleSlashCommand(command) {
158
268
 
159
269
  config.betaTools = enabledBetaTools;
160
270
  await saveConfig(config);
161
- providerInstance = createProvider(); // Re-init to update tools
271
+ if (providerInstance) {
272
+ const savedMessages = providerInstance.messages;
273
+ providerInstance = createProvider(); // Re-init to update tools
274
+ providerInstance.messages = savedMessages;
275
+ } else {
276
+ providerInstance = createProvider();
277
+ }
162
278
  console.log(chalk.green(`Beta tools updated: ${enabledBetaTools.join(', ') || 'none'}`));
163
279
  break;
280
+ case '/settings':
281
+ const { checkbox: settingsCheckbox } = await import('@inquirer/prompts');
282
+ const enabledSettings = await settingsCheckbox({
283
+ message: 'Select features to enable (Space to toggle, Enter to confirm):',
284
+ choices: [
285
+ {
286
+ name: 'Auto-feed workspace files to AI (uses .bananacodeignore / .gitignore)',
287
+ value: 'autoFeedWorkspace',
288
+ checked: config.autoFeedWorkspace || false
289
+ },
290
+ {
291
+ name: 'Use syntax highlighting for AI output (requires waiting for full response)',
292
+ value: 'useMarkedTerminal',
293
+ checked: config.useMarkedTerminal || false
294
+ },
295
+ {
296
+ name: 'Always show current token count in status bar',
297
+ value: 'showTokenCount',
298
+ checked: config.showTokenCount || false
299
+ }
300
+ ]
301
+ });
302
+
303
+ config.autoFeedWorkspace = enabledSettings.includes('autoFeedWorkspace');
304
+ config.useMarkedTerminal = enabledSettings.includes('useMarkedTerminal');
305
+ config.showTokenCount = enabledSettings.includes('showTokenCount');
306
+ await saveConfig(config);
307
+ if (providerInstance) {
308
+ const savedMessages = providerInstance.messages;
309
+ providerInstance = createProvider(); // Re-init to update tools/config
310
+ providerInstance.messages = savedMessages;
311
+ } else {
312
+ providerInstance = createProvider();
313
+ }
314
+ console.log(chalk.green(`Settings updated.`));
315
+ break;
164
316
  case '/debug':
165
317
  config.debug = !config.debug;
166
318
  await saveConfig(config);
167
- providerInstance = createProvider(); // Re-init to pass debug flag
319
+ if (providerInstance) {
320
+ const savedMessages = providerInstance.messages;
321
+ providerInstance = createProvider(); // Re-init to pass debug flag
322
+ providerInstance.messages = savedMessages;
323
+ } else {
324
+ providerInstance = createProvider();
325
+ }
168
326
  console.log(chalk.magenta(`Debug mode ${config.debug ? 'enabled' : 'disabled'}.`));
169
327
  break;
328
+ case '/skills':
329
+ const { getAvailableSkills } = await import('./utils/skills.js');
330
+ const skills = getAvailableSkills();
331
+ if (skills.length === 0) {
332
+ console.log(chalk.yellow("No skills found."));
333
+ const os = await import('os');
334
+ const path = await import('path');
335
+ const skillsDir = path.join(os.homedir(), '.config', 'banana-code', 'skills');
336
+ console.log(chalk.gray(`Create skill directories with a SKILL.md file in ${skillsDir}`));
337
+ } else {
338
+ console.log(chalk.cyan.bold("\nLoaded Skills:"));
339
+ skills.forEach(skill => {
340
+ console.log(chalk.green(`- ${skill.id}`) + `: ${skill.description}`);
341
+ });
342
+ }
343
+ break;
344
+ case '/plan':
345
+ config.planMode = true;
346
+ await saveConfig(config);
347
+ if (providerInstance) {
348
+ const savedMessages = providerInstance.messages;
349
+ providerInstance = createProvider();
350
+ providerInstance.messages = savedMessages;
351
+ } else {
352
+ providerInstance = createProvider();
353
+ }
354
+ console.log(chalk.magenta(`Plan mode enabled. For significant changes, the AI will now propose an implementation plan before writing code.`));
355
+ break;
356
+ case '/agent':
357
+ config.planMode = false;
358
+ await saveConfig(config);
359
+ if (providerInstance) {
360
+ const savedMessages = providerInstance.messages;
361
+ providerInstance = createProvider();
362
+ providerInstance.messages = savedMessages;
363
+ } else {
364
+ providerInstance = createProvider();
365
+ }
366
+ console.log(chalk.green(`Agent mode enabled. The AI will make changes directly.`));
367
+ break;
170
368
  case '/chats':
171
369
  const sessions = await listSessions();
172
370
  if (sessions.length === 0) {
@@ -183,13 +381,18 @@ async function handleSlashCommand(command) {
183
381
  case '/help':
184
382
  console.log(chalk.yellow(`
185
383
  Available commands:
186
- /provider <name> - Switch AI provider (gemini, claude, openai, ollama)
384
+ /provider <name> - Switch AI provider (gemini, claude, openai, ollama_cloud, ollama)
187
385
  /model [name] - Switch model within current provider (opens menu if name omitted)
188
386
  /chats - List persistent chat sessions
189
387
  /clear - Clear chat history
388
+ /clean - Compress chat history into a summary to save tokens
190
389
  /context - Show current context window size
191
390
  /permissions - List session-approved permissions
192
391
  /beta - Manage beta features and tools
392
+ /settings - Manage app settings (workspace auto-feed, etc)
393
+ /skills - List loaded agent skills
394
+ /plan - Enable Plan Mode (AI proposes a plan for big changes)
395
+ /agent - Enable Agent Mode (default, AI edits directly)
193
396
  /debug - Toggle debug mode (show tool results)
194
397
  /help - Show all commands
195
398
  /exit - Quit Banana Code
@@ -229,7 +432,7 @@ function drawPromptBox(inputText, cursorPos) {
229
432
  const placeholder = 'Type your message or @path/to/file';
230
433
  const prefix = ' > ';
231
434
 
232
- const visibleText = (inputText.length === 0) ? (prefix + chalk.gray(placeholder)) : (prefix + inputText);
435
+ const visibleText = (inputText.length === 0) ? (prefix + chalk.gray(placeholder)) : (prefix + chalk.white(inputText));
233
436
  const totalChars = (prefix.length + Math.max(inputText.length, placeholder.length));
234
437
  const rows = Math.ceil(totalChars / width) || 1;
235
438
 
@@ -249,8 +452,26 @@ function drawPromptBox(inputText, cursorPos) {
249
452
  // Redraw status bar and separator (they are always below the prompt)
250
453
  const modelDisplay = providerInstance ? providerInstance.modelName : (config.model || 'unknown');
251
454
  const providerDisplay = config.provider.toUpperCase();
252
- const leftText = ` Provider: ${chalk.cyan(providerDisplay)} / Model: ${chalk.yellow(modelDisplay)}`;
253
- const rightText = '? for shortcuts ';
455
+ const modeDisplay = config.planMode ? chalk.magenta('PLAN MODE') : chalk.green('AGENT MODE');
456
+
457
+ let tokenDisplay = '';
458
+ if (config.showTokenCount && providerInstance) {
459
+ let msgs = providerInstance.messages || [];
460
+ // Support for Ollama chat history format if different
461
+ if (!providerInstance.messages && typeof providerInstance.chat?.getHistory === 'function') {
462
+ msgs = providerInstance.chat.getHistory(); // Note: this is async normally, but we use an approximation here or just skip it if it's strictly async. For now, assume providerInstance.messages is the standard.
463
+ }
464
+ const tokens = estimateConversationTokens(msgs);
465
+ let color = chalk.green;
466
+ if (tokens >= 128000) color = chalk.red;
467
+ else if (tokens >= 86000) color = chalk.hex('#FFA500'); // Orange
468
+ else if (tokens >= 64000) color = chalk.yellow;
469
+
470
+ tokenDisplay = ` / Tokens: ${color(tokens.toLocaleString())}`;
471
+ }
472
+
473
+ const leftText = ` Provider: ${chalk.cyan(providerDisplay)} / Model: ${chalk.yellow(modelDisplay)} / ${modeDisplay}${tokenDisplay}`;
474
+ const rightText = '/help for shortcuts ';
254
475
  const leftStripped = leftText.replace(/\x1b\[[0-9;]*m/g, '');
255
476
  const midPad = Math.max(0, width - leftStripped.length - rightText.length);
256
477
  const statusLine = chalk.gray(leftText + ' '.repeat(midPad) + rightText);
@@ -276,7 +497,7 @@ function drawPromptBoxInitial(inputText) {
276
497
  const placeholder = 'Type your message or @path/to/file';
277
498
  const prefix = ' > ';
278
499
 
279
- const visibleText = (inputText.length === 0) ? (prefix + chalk.gray(placeholder)) : (prefix + inputText);
500
+ const visibleText = (inputText.length === 0) ? (prefix + chalk.gray(placeholder)) : (prefix + chalk.white(inputText));
280
501
  const totalChars = (prefix.length + Math.max(inputText.length, placeholder.length));
281
502
  const rows = Math.ceil(totalChars / width) || 1;
282
503
 
@@ -289,11 +510,29 @@ function drawPromptBoxInitial(inputText) {
289
510
  process.stdout.write(userBg(padLine(lineText, width)) + '\n');
290
511
  }
291
512
 
292
- // Status bar: Current Provider / Model + right-aligned "? for shortcuts"
513
+ // Status bar: Current Provider / Model + right-aligned "/help for shortcuts"
293
514
  const modelDisplay = providerInstance ? providerInstance.modelName : (config.model || 'unknown');
294
515
  const providerDisplay = config.provider.toUpperCase();
295
- const leftText = ` Provider: ${chalk.cyan(providerDisplay)} / Model: ${chalk.yellow(modelDisplay)}`;
296
- const rightText = '? for shortcuts ';
516
+ const modeDisplay = config.planMode ? chalk.magenta('PLAN MODE') : chalk.green('AGENT MODE');
517
+
518
+ let tokenDisplay = '';
519
+ if (config.showTokenCount && providerInstance) {
520
+ let msgs = providerInstance.messages || [];
521
+ // Support for Ollama chat history format if different
522
+ if (!providerInstance.messages && typeof providerInstance.chat?.getHistory === 'function') {
523
+ msgs = providerInstance.chat.getHistory(); // Note: this is async normally, but we use an approximation here or just skip it if it's strictly async. For now, assume providerInstance.messages is the standard.
524
+ }
525
+ const tokens = estimateConversationTokens(msgs);
526
+ let color = chalk.green;
527
+ if (tokens >= 128000) color = chalk.red;
528
+ else if (tokens >= 86000) color = chalk.hex('#FFA500'); // Orange
529
+ else if (tokens >= 64000) color = chalk.yellow;
530
+
531
+ tokenDisplay = ` / Tokens: ${color(tokens.toLocaleString())}`;
532
+ }
533
+
534
+ const leftText = ` Provider: ${chalk.cyan(providerDisplay)} / Model: ${chalk.yellow(modelDisplay)} / ${modeDisplay}${tokenDisplay}`;
535
+ const rightText = '/help for shortcuts ';
297
536
 
298
537
  const leftStripped = leftText.replace(/\x1b\[[0-9;]*m/g, '');
299
538
  const midPad = Math.max(0, width - leftStripped.length - rightText.length);
@@ -378,6 +617,10 @@ function promptUser() {
378
617
 
379
618
  if (str === '\r' || str === '\n') { // Enter
380
619
  exitRequested = false;
620
+ if (inputBuffer.trim() && inputBuffer !== commandHistory[commandHistory.length - 1]) {
621
+ commandHistory.push(inputBuffer);
622
+ }
623
+ historyIndex = -1;
381
624
  resolve(inputBuffer);
382
625
  return;
383
626
  }
@@ -411,6 +654,33 @@ function promptUser() {
411
654
  return;
412
655
  }
413
656
 
657
+ if (str === '\x1b[A') { // Arrow Up
658
+ if (historyIndex === -1) {
659
+ currentInputSaved = inputBuffer;
660
+ }
661
+ if (historyIndex < commandHistory.length - 1) {
662
+ historyIndex++;
663
+ inputBuffer = commandHistory[commandHistory.length - 1 - historyIndex];
664
+ cursorPos = inputBuffer.length;
665
+ drawPromptBox(inputBuffer, cursorPos);
666
+ }
667
+ return;
668
+ }
669
+
670
+ if (str === '\x1b[B') { // Arrow Down
671
+ if (historyIndex > -1) {
672
+ historyIndex--;
673
+ if (historyIndex === -1) {
674
+ inputBuffer = currentInputSaved;
675
+ } else {
676
+ inputBuffer = commandHistory[commandHistory.length - 1 - historyIndex];
677
+ }
678
+ cursorPos = inputBuffer.length;
679
+ drawPromptBox(inputBuffer, cursorPos);
680
+ }
681
+ return;
682
+ }
683
+
414
684
  if (str === '\x1b[H' || str === '\x01') { // Home / Ctrl+A
415
685
  cursorPos = 0; drawPromptBox(inputBuffer, cursorPos);
416
686
  return;
@@ -462,20 +732,23 @@ async function main() {
462
732
  for (const msg of session.messages) {
463
733
  if (msg.role === 'system') continue;
464
734
 
465
- if (session.provider === 'gemini') {
735
+ if (config.provider === 'gemini') {
466
736
  if (msg.role === 'user') {
467
737
  if (msg.parts[0]?.text) console.log(`${chalk.yellow('🍌 >')} ${msg.parts[0].text}`);
468
738
  else if (msg.parts[0]?.functionResponse) {
469
- console.log(chalk.yellow(`[Tool Result: ${msg.parts[0].functionResponse.name}]`));
739
+ console.log(chalk.yellow(`[Tool Result Received]`));
470
740
  }
471
741
  } else if (msg.role === 'model') {
472
742
  msg.parts.forEach(p => {
473
- if (p.text) process.stdout.write(chalk.cyan(p.text));
743
+ if (p.text) {
744
+ if (config.useMarkedTerminal) printMarkdown(p.text);
745
+ else process.stdout.write(chalk.cyan(p.text));
746
+ }
474
747
  if (p.functionCall) console.log(chalk.yellow(`\n[Banana Calling Tool: ${p.functionCall.name}]`));
475
748
  });
476
749
  console.log();
477
750
  }
478
- } else if (session.provider === 'claude') {
751
+ } else if (config.provider === 'claude') {
479
752
  if (msg.role === 'user') {
480
753
  if (typeof msg.content === 'string') console.log(`${chalk.yellow('🍌 >')} ${msg.content}`);
481
754
  else {
@@ -484,26 +757,36 @@ async function main() {
484
757
  });
485
758
  }
486
759
  } else if (msg.role === 'assistant') {
487
- if (typeof msg.content === 'string') console.log(chalk.cyan(msg.content));
488
- else {
760
+ if (typeof msg.content === 'string') {
761
+ if (config.useMarkedTerminal) printMarkdown(msg.content);
762
+ else process.stdout.write(chalk.cyan(msg.content));
763
+ } else {
489
764
  msg.content.forEach(c => {
490
- if (c.type === 'text') process.stdout.write(chalk.cyan(c.text));
765
+ if (c.type === 'text') {
766
+ if (config.useMarkedTerminal) printMarkdown(c.text);
767
+ else process.stdout.write(chalk.cyan(c.text));
768
+ }
491
769
  if (c.type === 'tool_use') console.log(chalk.yellow(`\n[Banana Calling Tool: ${c.name}]`));
492
770
  });
493
- console.log();
494
771
  }
772
+ console.log();
495
773
  }
496
774
  } else {
497
775
  // OpenAI, Ollama
498
776
  if (msg.role === 'user') {
499
777
  console.log(`${chalk.yellow('🍌 >')} ${msg.content}`);
500
778
  } else if (msg.role === 'assistant' || msg.role === 'output_text') {
501
- if (msg.content) console.log(chalk.cyan(msg.content));
779
+ if (msg.content) {
780
+ if (config.useMarkedTerminal) printMarkdown(msg.content);
781
+ else process.stdout.write(chalk.cyan(msg.content));
782
+ }
502
783
  if (msg.tool_calls) {
503
784
  msg.tool_calls.forEach(tc => {
504
- console.log(chalk.yellow(`[Banana Calling Tool: ${tc.function.name}]`));
785
+ const name = tc.function ? tc.function.name : tc.name;
786
+ console.log(chalk.yellow(`\n[Banana Calling Tool: ${name}]`));
505
787
  });
506
788
  }
789
+ console.log();
507
790
  } else if (msg.role === 'tool') {
508
791
  console.log(chalk.yellow(`[Tool Result Received]`));
509
792
  }
@@ -535,8 +818,49 @@ async function main() {
535
818
  if (trimmed.startsWith('/')) {
536
819
  await handleSlashCommand(trimmed);
537
820
  } else {
821
+ let finalInput = trimmed;
822
+ const fileMentions = trimmed.match(/@@?([\w/.-]+)/g);
823
+ if (fileMentions) {
824
+ let addedFiles = 0;
825
+ const fsSync = await import('fs');
826
+ const path = await import('path');
827
+ for (const mention of fileMentions) {
828
+ let filepath;
829
+ if (mention.startsWith('@@')) {
830
+ filepath = mention.substring(2);
831
+ } else {
832
+ filepath = path.join(process.cwd(), mention.substring(1));
833
+ }
834
+
835
+ try {
836
+ const stat = fsSync.statSync(filepath);
837
+ if (stat.isFile()) {
838
+ const content = fsSync.readFileSync(filepath, 'utf8');
839
+ finalInput += `\n\n--- File Context: ${filepath} ---\n${content}\n--- End of ${filepath} ---`;
840
+ addedFiles++;
841
+ }
842
+ } catch (e) {
843
+ console.log(chalk.yellow(`Warning: Could not read file for mention ${mention}`));
844
+ }
845
+ }
846
+ if (addedFiles > 0) {
847
+ console.log(chalk.gray(`(Attached ${addedFiles} file(s) to context)`));
848
+ }
849
+ }
850
+
851
+ if (config.autoFeedWorkspace) {
852
+ const { getWorkspaceTree } = await import('./utils/workspace.js');
853
+ const tree = await getWorkspaceTree();
854
+ const { getSystemPrompt } = await import('./prompt.js');
855
+ let newSysPrompt = getSystemPrompt(config);
856
+ newSysPrompt += `\n\n--- Workspace File Tree ---\n${tree}\n--- End of Tree ---`;
857
+ if (typeof providerInstance.updateSystemPrompt === 'function') {
858
+ providerInstance.updateSystemPrompt(newSysPrompt);
859
+ }
860
+ }
861
+
538
862
  process.stdout.write(chalk.cyan('✦ '));
539
- await providerInstance.sendMessage(trimmed);
863
+ await providerInstance.sendMessage(finalInput);
540
864
  console.log(); // Extra newline after AI response
541
865
  // Save session after AI message
542
866
  await saveSession(currentSessionId, {
package/src/prompt.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import os from 'os';
2
2
  import { getAvailableTools } from './tools/registry.js';
3
+ import { getAvailableSkills } from './utils/skills.js';
3
4
 
4
5
  export function getSystemPrompt(config = {}) {
5
6
  const platform = os.platform();
@@ -16,6 +17,7 @@ export function getSystemPrompt(config = {}) {
16
17
  const availableToolsList = getAvailableTools(config);
17
18
  const availableToolsNames = availableToolsList.map(t => t.name).join(', ');
18
19
  const hasPatchTool = availableToolsList.some(t => t.name === 'patch_file');
20
+ const skills = getAvailableSkills();
19
21
 
20
22
  let prompt = `You are Banana Code, a terminal-based AI coding assistant running on ${osDescription}. You help users write, debug, and understand code. You have access to tools: ${availableToolsNames}.
21
23
 
@@ -27,6 +29,26 @@ SAFETY RULES:
27
29
 
28
30
  Always use tools when they would help. Be concise but thorough. `;
29
31
 
32
+ if (skills && skills.length > 0) {
33
+ 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
+ for (const skill of skills) {
35
+ prompt += ` <skill>\n <name>${skill.id}</name>\n <description>${skill.description}</description>\n </skill>\n`;
36
+ }
37
+ prompt += `</available_skills>\n\nOnce a skill is activated, its instructions and resources are returned wrapped in <activated_skill> tags. You MUST treat the content within <instructions> as expert procedural guidance for the duration of the task.\n`;
38
+ }
39
+
40
+ if (config.planMode) {
41
+ prompt += `
42
+ [PLAN MODE ENABLED]
43
+ The user is operating in "Plan Mode".
44
+ - For very small, trivial changes (like fixing a typo or a one-line bug), you may execute the change directly using your tools.
45
+ - For ANY change that has a significant impact, modifies multiple areas, or adds a new feature, you MUST NOT write or patch code immediately.
46
+ - Instead, you MUST output a detailed "Implementation Plan" outlining the files you will change and the specific steps you will take.
47
+ - Stop and ask the user: "Does this plan look good, or would you like to make any changes?"
48
+ - ONLY proceed to use the 'write_file' or 'patch_file' tools AFTER the user has explicitly approved the plan.
49
+ `;
50
+ }
51
+
30
52
  if (hasPatchTool) {
31
53
  prompt += `
32
54
  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.`;