@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/README.md +35 -5
- package/package.json +15 -1
- package/src/config.js +24 -2
- package/src/constants.js +11 -0
- package/src/index.js +360 -36
- package/src/prompt.js +22 -0
- package/src/providers/claude.js +13 -2
- package/src/providers/gemini.js +15 -2
- package/src/providers/ollama.js +13 -1
- package/src/providers/ollamaCloud.js +107 -0
- package/src/providers/openai.js +37 -17
- package/src/tools/activateSkill.js +22 -0
- package/src/tools/execCommand.js +6 -2
- package/src/tools/registry.js +13 -0
- package/src/utils/markdown.js +21 -0
- package/src/utils/skills.js +61 -0
- package/src/utils/tokens.js +44 -0
- package/src/utils/workspace.js +30 -0
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
135
|
-
choices:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
253
|
-
|
|
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 "
|
|
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
|
|
296
|
-
|
|
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 (
|
|
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
|
|
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)
|
|
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 (
|
|
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')
|
|
488
|
-
|
|
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')
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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.`;
|