@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/providers/claude.js
CHANGED
|
@@ -3,9 +3,11 @@ import { getAvailableTools, executeTool } from '../tools/registry.js';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import { getSystemPrompt } from '../prompt.js';
|
|
6
|
+
import { printMarkdown } from '../utils/markdown.js';
|
|
6
7
|
|
|
7
8
|
export class ClaudeProvider {
|
|
8
9
|
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
9
11
|
this.anthropic = new Anthropic({ apiKey: config.apiKey });
|
|
10
12
|
this.modelName = config.model || 'claude-3-7-sonnet-20250219';
|
|
11
13
|
this.messages = [];
|
|
@@ -17,6 +19,10 @@ export class ClaudeProvider {
|
|
|
17
19
|
this.systemPrompt = getSystemPrompt(config);
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
updateSystemPrompt(newPrompt) {
|
|
23
|
+
this.systemPrompt = newPrompt;
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
async sendMessage(message) {
|
|
21
27
|
this.messages.push({ role: 'user', content: message });
|
|
22
28
|
|
|
@@ -47,8 +53,10 @@ export class ClaudeProvider {
|
|
|
47
53
|
|
|
48
54
|
for await (const event of stream) {
|
|
49
55
|
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
|
|
50
|
-
if (spinner.isSpinning) spinner.stop();
|
|
51
|
-
|
|
56
|
+
if (spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
|
|
57
|
+
if (!this.config.useMarkedTerminal) {
|
|
58
|
+
process.stdout.write(chalk.cyan(event.delta.text));
|
|
59
|
+
}
|
|
52
60
|
chunkResponse += event.delta.text;
|
|
53
61
|
finalResponse += event.delta.text;
|
|
54
62
|
} else if (event.type === 'content_block_start' && event.content_block.type === 'tool_use') {
|
|
@@ -76,8 +84,11 @@ export class ClaudeProvider {
|
|
|
76
84
|
}
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
if (spinner.isSpinning) spinner.stop();
|
|
88
|
+
|
|
79
89
|
const newContent = [];
|
|
80
90
|
if (chunkResponse) {
|
|
91
|
+
if (this.config.useMarkedTerminal) printMarkdown(chunkResponse);
|
|
81
92
|
newContent.push({ type: 'text', text: chunkResponse });
|
|
82
93
|
}
|
|
83
94
|
|
package/src/providers/gemini.js
CHANGED
|
@@ -2,6 +2,7 @@ import { getAvailableTools, executeTool } from '../tools/registry.js';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { getSystemPrompt } from '../prompt.js';
|
|
5
|
+
import { printMarkdown } from '../utils/markdown.js';
|
|
5
6
|
|
|
6
7
|
export class GeminiProvider {
|
|
7
8
|
constructor(config) {
|
|
@@ -17,6 +18,10 @@ export class GeminiProvider {
|
|
|
17
18
|
this.systemPrompt = getSystemPrompt(config);
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
updateSystemPrompt(newPrompt) {
|
|
22
|
+
this.systemPrompt = newPrompt;
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
async sendMessage(message) {
|
|
21
26
|
this.messages.push({ role: 'user', parts: [{ text: message }] });
|
|
22
27
|
|
|
@@ -25,6 +30,7 @@ export class GeminiProvider {
|
|
|
25
30
|
|
|
26
31
|
try {
|
|
27
32
|
while (true) {
|
|
33
|
+
let currentTurnText = '';
|
|
28
34
|
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${this.modelName}:streamGenerateContent?alt=sse&key=${this.apiKey}`, {
|
|
29
35
|
method: 'POST',
|
|
30
36
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -74,9 +80,12 @@ export class GeminiProvider {
|
|
|
74
80
|
if (content && content.parts) {
|
|
75
81
|
for (const part of content.parts) {
|
|
76
82
|
if (part.text) {
|
|
77
|
-
if (spinner && spinner.isSpinning) spinner.stop();
|
|
78
|
-
|
|
83
|
+
if (spinner && spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
|
|
84
|
+
if (!this.config.useMarkedTerminal) {
|
|
85
|
+
process.stdout.write(chalk.cyan(part.text));
|
|
86
|
+
}
|
|
79
87
|
responseText += part.text;
|
|
88
|
+
currentTurnText += part.text;
|
|
80
89
|
|
|
81
90
|
// Aggregate sequential text parts
|
|
82
91
|
let lastPart = aggregatedParts[aggregatedParts.length - 1];
|
|
@@ -109,6 +118,10 @@ export class GeminiProvider {
|
|
|
109
118
|
|
|
110
119
|
if (spinner && spinner.isSpinning) spinner.stop();
|
|
111
120
|
|
|
121
|
+
if (currentTurnText && this.config.useMarkedTerminal) {
|
|
122
|
+
printMarkdown(currentTurnText);
|
|
123
|
+
}
|
|
124
|
+
|
|
112
125
|
if (aggregatedParts.length === 0) break;
|
|
113
126
|
|
|
114
127
|
// Push exact unmutated model response back to history
|
package/src/providers/ollama.js
CHANGED
|
@@ -2,6 +2,7 @@ import { getAvailableTools, executeTool } from '../tools/registry.js';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { getSystemPrompt } from '../prompt.js';
|
|
5
|
+
import { printMarkdown } from '../utils/markdown.js';
|
|
5
6
|
|
|
6
7
|
export class OllamaProvider {
|
|
7
8
|
constructor(config) {
|
|
@@ -20,6 +21,13 @@ export class OllamaProvider {
|
|
|
20
21
|
this.URL = 'http://localhost:11434/api/chat';
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
updateSystemPrompt(newPrompt) {
|
|
25
|
+
this.systemPrompt = newPrompt;
|
|
26
|
+
if (this.messages.length > 0 && this.messages[0].role === 'system') {
|
|
27
|
+
this.messages[0].content = newPrompt;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
async sendMessage(message) {
|
|
24
32
|
this.messages.push({ role: 'user', content: message });
|
|
25
33
|
|
|
@@ -49,7 +57,11 @@ export class OllamaProvider {
|
|
|
49
57
|
const messageObj = data.message;
|
|
50
58
|
|
|
51
59
|
if (messageObj.content) {
|
|
52
|
-
|
|
60
|
+
if (this.config.useMarkedTerminal) {
|
|
61
|
+
printMarkdown(messageObj.content);
|
|
62
|
+
} else {
|
|
63
|
+
process.stdout.write(chalk.cyan(messageObj.content));
|
|
64
|
+
}
|
|
53
65
|
finalResponse += messageObj.content;
|
|
54
66
|
}
|
|
55
67
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { getAvailableTools, executeTool } from '../tools/registry.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { getSystemPrompt } from '../prompt.js';
|
|
5
|
+
import { printMarkdown } from '../utils/markdown.js';
|
|
6
|
+
|
|
7
|
+
export class OllamaCloudProvider {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.apiKey = config.apiKey;
|
|
11
|
+
this.modelName = config.model || 'llama3.3';
|
|
12
|
+
this.systemPrompt = getSystemPrompt(config);
|
|
13
|
+
this.messages = [{ role: 'system', content: this.systemPrompt }];
|
|
14
|
+
this.tools = getAvailableTools(config).map(t => ({
|
|
15
|
+
type: 'function',
|
|
16
|
+
function: {
|
|
17
|
+
name: t.name,
|
|
18
|
+
description: t.description,
|
|
19
|
+
parameters: t.parameters
|
|
20
|
+
}
|
|
21
|
+
}));
|
|
22
|
+
this.URL = 'https://ollama.com/api/chat';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
updateSystemPrompt(newPrompt) {
|
|
26
|
+
this.systemPrompt = newPrompt;
|
|
27
|
+
if (this.messages.length > 0 && this.messages[0].role === 'system') {
|
|
28
|
+
this.messages[0].content = newPrompt;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async sendMessage(message) {
|
|
33
|
+
this.messages.push({ role: 'user', content: message });
|
|
34
|
+
|
|
35
|
+
let spinner = ora({ text: 'Thinking (Cloud)...', color: 'yellow', stream: process.stdout }).start();
|
|
36
|
+
let finalResponse = '';
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
while (true) {
|
|
40
|
+
const response = await fetch(this.URL, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
model: this.modelName,
|
|
48
|
+
messages: this.messages,
|
|
49
|
+
tools: this.tools.length > 0 ? this.tools : undefined,
|
|
50
|
+
stream: false // Cloud API sometimes prefers non-streaming or different SSE formats
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const errorText = await response.text();
|
|
56
|
+
spinner.stop();
|
|
57
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const data = await response.json();
|
|
61
|
+
spinner.stop();
|
|
62
|
+
|
|
63
|
+
const messageObj = data.message;
|
|
64
|
+
|
|
65
|
+
if (messageObj.content) {
|
|
66
|
+
if (this.config.useMarkedTerminal) {
|
|
67
|
+
printMarkdown(messageObj.content);
|
|
68
|
+
} else {
|
|
69
|
+
process.stdout.write(chalk.cyan(messageObj.content));
|
|
70
|
+
}
|
|
71
|
+
finalResponse += messageObj.content;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.messages.push(messageObj);
|
|
75
|
+
|
|
76
|
+
if (!messageObj.tool_calls || messageObj.tool_calls.length === 0) {
|
|
77
|
+
console.log();
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const call of messageObj.tool_calls) {
|
|
82
|
+
const fn = call.function;
|
|
83
|
+
console.log(chalk.yellow(`\n[Banana Calling Tool: ${fn.name}]`));
|
|
84
|
+
|
|
85
|
+
let res = await executeTool(fn.name, fn.arguments);
|
|
86
|
+
if (this.config.debug) {
|
|
87
|
+
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
88
|
+
}
|
|
89
|
+
console.log(chalk.yellow(`[Tool Result Received]\n`));
|
|
90
|
+
|
|
91
|
+
this.messages.push({
|
|
92
|
+
role: 'tool',
|
|
93
|
+
content: typeof res === 'string' ? res : JSON.stringify(res)
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
spinner = ora({ text: 'Processing tool results...', color: 'yellow', stream: process.stdout }).start();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return finalResponse;
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (spinner.isSpinning) spinner.stop();
|
|
103
|
+
console.error(chalk.red(`Ollama Cloud Error: ${err.message}`));
|
|
104
|
+
return `Error: ${err.message}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/providers/openai.js
CHANGED
|
@@ -6,6 +6,7 @@ import os from 'os';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import fsSync from 'fs';
|
|
8
8
|
import { getSystemPrompt } from '../prompt.js';
|
|
9
|
+
import { printMarkdown } from '../utils/markdown.js';
|
|
9
10
|
|
|
10
11
|
export class OpenAIProvider {
|
|
11
12
|
constructor(config) {
|
|
@@ -26,6 +27,13 @@ export class OpenAIProvider {
|
|
|
26
27
|
}));
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
updateSystemPrompt(newPrompt) {
|
|
31
|
+
this.systemPrompt = newPrompt;
|
|
32
|
+
if (this.messages.length > 0 && this.messages[0].role === 'system') {
|
|
33
|
+
this.messages[0].content = newPrompt;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
async sendMessage(message) {
|
|
30
38
|
if (this.config.authType === 'oauth') {
|
|
31
39
|
return await this.sendOauthMessage(message);
|
|
@@ -59,8 +67,10 @@ export class OpenAIProvider {
|
|
|
59
67
|
const delta = chunk.choices[0]?.delta;
|
|
60
68
|
|
|
61
69
|
if (delta?.content) {
|
|
62
|
-
if (spinner.isSpinning) spinner.stop();
|
|
63
|
-
|
|
70
|
+
if (spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
|
|
71
|
+
if (!this.config.useMarkedTerminal) {
|
|
72
|
+
process.stdout.write(chalk.cyan(delta.content));
|
|
73
|
+
}
|
|
64
74
|
chunkResponse += delta.content;
|
|
65
75
|
finalResponse += delta.content;
|
|
66
76
|
}
|
|
@@ -87,6 +97,7 @@ export class OpenAIProvider {
|
|
|
87
97
|
if (spinner.isSpinning) spinner.stop();
|
|
88
98
|
|
|
89
99
|
if (chunkResponse) {
|
|
100
|
+
if (this.config.useMarkedTerminal) printMarkdown(chunkResponse);
|
|
90
101
|
this.messages.push({ role: 'assistant', content: chunkResponse });
|
|
91
102
|
}
|
|
92
103
|
|
|
@@ -206,6 +217,18 @@ export class OpenAIProvider {
|
|
|
206
217
|
const backendInput = mapMessagesToBackend(this.messages);
|
|
207
218
|
const backendTools = mapToolsToBackend(this.tools);
|
|
208
219
|
|
|
220
|
+
const payload = {
|
|
221
|
+
model: this.modelName || 'gpt-5.1-codex',
|
|
222
|
+
instructions: this.systemPrompt,
|
|
223
|
+
input: backendInput,
|
|
224
|
+
tools: backendTools,
|
|
225
|
+
store: false,
|
|
226
|
+
stream: true,
|
|
227
|
+
include: ["reasoning.encrypted_content"],
|
|
228
|
+
reasoning: { effort: "medium", summary: "auto" },
|
|
229
|
+
text: { verbosity: "medium" }
|
|
230
|
+
};
|
|
231
|
+
|
|
209
232
|
const response = await fetch('https://chatgpt.com/backend-api/codex/responses', {
|
|
210
233
|
method: 'POST',
|
|
211
234
|
headers: {
|
|
@@ -216,17 +239,7 @@ export class OpenAIProvider {
|
|
|
216
239
|
'originator': 'codex_cli_rs',
|
|
217
240
|
'Accept': 'text/event-stream'
|
|
218
241
|
},
|
|
219
|
-
body: JSON.stringify(
|
|
220
|
-
model: this.modelName || 'gpt-5.1-codex',
|
|
221
|
-
instructions: this.systemPrompt,
|
|
222
|
-
input: backendInput,
|
|
223
|
-
tools: backendTools,
|
|
224
|
-
store: false,
|
|
225
|
-
stream: true,
|
|
226
|
-
include: ["reasoning.encrypted_content"],
|
|
227
|
-
reasoning: { effort: "medium", summary: "auto" },
|
|
228
|
-
text: { verbosity: "medium" }
|
|
229
|
-
})
|
|
242
|
+
body: JSON.stringify(payload)
|
|
230
243
|
});
|
|
231
244
|
|
|
232
245
|
if (!response.ok) {
|
|
@@ -259,10 +272,17 @@ export class OpenAIProvider {
|
|
|
259
272
|
try {
|
|
260
273
|
const data = JSON.parse(currentDataBuffer);
|
|
261
274
|
if (currentEvent === 'response.output_text.delta') {
|
|
262
|
-
if (spinner && spinner.isSpinning) spinner.stop();
|
|
263
|
-
|
|
275
|
+
if (spinner && spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
|
|
276
|
+
if (!this.config.useMarkedTerminal) {
|
|
277
|
+
process.stdout.write(chalk.cyan(data.delta));
|
|
278
|
+
}
|
|
264
279
|
currentChunkResponse += data.delta;
|
|
265
280
|
finalResponse += data.delta;
|
|
281
|
+
} else if (currentEvent === 'response.reasoning.delta' || currentEvent === 'response.reasoning_text.delta' || currentEvent.includes('reasoning.delta')) {
|
|
282
|
+
if (this.config.debug && data.delta) {
|
|
283
|
+
if (spinner && spinner.isSpinning) spinner.stop();
|
|
284
|
+
process.stdout.write(chalk.gray(data.delta));
|
|
285
|
+
}
|
|
266
286
|
} else if (currentEvent === 'response.output_item.added' && data.item?.type === 'function_call') {
|
|
267
287
|
if (spinner && spinner.isSpinning) spinner.stop();
|
|
268
288
|
currentToolCall = {
|
|
@@ -276,7 +296,7 @@ export class OpenAIProvider {
|
|
|
276
296
|
if (!spinner.isSpinning) {
|
|
277
297
|
spinner = ora({ text: `Generating ${chalk.yellow(currentToolCall.name)} (${currentToolCall.arguments.length} bytes)...`, color: 'yellow', stream: process.stdout }).start();
|
|
278
298
|
} else {
|
|
279
|
-
spinner.text = `Generating ${chalk.yellow(currentToolCall.name)} (${currentToolCall.arguments.length} bytes)...`;
|
|
299
|
+
spinner.text = `Generating ${chalk.yellow(currentToolCall.name)} arguments (${currentToolCall.arguments.length} bytes)...`;
|
|
280
300
|
}
|
|
281
301
|
} else if (currentEvent === 'response.output_item.done' && data.item?.type === 'function_call' && currentToolCall) {
|
|
282
302
|
if (spinner && spinner.isSpinning) spinner.stop();
|
|
@@ -322,7 +342,6 @@ export class OpenAIProvider {
|
|
|
322
342
|
currentChunkResponse += data.delta;
|
|
323
343
|
finalResponse += data.delta;
|
|
324
344
|
}
|
|
325
|
-
// Note: tool calls usually don't end exactly at stream end without \n\n
|
|
326
345
|
} catch (e) { }
|
|
327
346
|
}
|
|
328
347
|
}
|
|
@@ -330,6 +349,7 @@ export class OpenAIProvider {
|
|
|
330
349
|
if (spinner.isSpinning) spinner.stop();
|
|
331
350
|
|
|
332
351
|
if (currentChunkResponse) {
|
|
352
|
+
if (this.config.useMarkedTerminal) printMarkdown(currentChunkResponse);
|
|
333
353
|
this.messages.push({ role: 'assistant', content: currentChunkResponse });
|
|
334
354
|
}
|
|
335
355
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getAvailableSkills } from '../utils/skills.js';
|
|
2
|
+
|
|
3
|
+
export async function activateSkill({ skillName }) {
|
|
4
|
+
const skills = getAvailableSkills();
|
|
5
|
+
// Match by ID or Name
|
|
6
|
+
const skill = skills.find(s => s.id === skillName || s.name === skillName);
|
|
7
|
+
|
|
8
|
+
if (!skill) {
|
|
9
|
+
return `Error: Skill '${skillName}' not found. Available skills: ${skills.map(s => s.name).join(', ')}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// The format expected by the AI agent
|
|
13
|
+
let output = `<activated_skill>\n`;
|
|
14
|
+
output += `<instructions>\n${skill.instructions}\n</instructions>\n`;
|
|
15
|
+
output += `<available_resources>\n`;
|
|
16
|
+
output += `- location: ${skill.path}\n`;
|
|
17
|
+
output += ` (Use list_directory and read_file to access bundled scripts, references, or assets)\n`;
|
|
18
|
+
output += `</available_resources>\n`;
|
|
19
|
+
output += `</activated_skill>`;
|
|
20
|
+
|
|
21
|
+
return output;
|
|
22
|
+
}
|
package/src/tools/execCommand.js
CHANGED
|
@@ -29,12 +29,16 @@ export async function execCommand({ command, cwd = process.cwd() }) {
|
|
|
29
29
|
|
|
30
30
|
child.on('close', (code) => {
|
|
31
31
|
if (spinner.isSpinning) spinner.stop();
|
|
32
|
-
|
|
32
|
+
let result = `Command exited with code ${code}.\nOutput:\n${output}`;
|
|
33
|
+
if (code !== 0) {
|
|
34
|
+
result += `\n\n[System Note: The command failed with an error. Please analyze the output above and try to fix the issue if it is possible.]`;
|
|
35
|
+
}
|
|
36
|
+
resolve(result);
|
|
33
37
|
});
|
|
34
38
|
|
|
35
39
|
child.on('error', (err) => {
|
|
36
40
|
if (spinner.isSpinning) spinner.stop();
|
|
37
|
-
resolve(`Error executing command: ${err.message}`);
|
|
41
|
+
resolve(`Error executing command: ${err.message}\n\n[System Note: The command failed to execute. Please analyze the error and try to fix the issue if it is possible.]`);
|
|
38
42
|
});
|
|
39
43
|
});
|
|
40
44
|
}
|
package/src/tools/registry.js
CHANGED
|
@@ -7,6 +7,7 @@ import { listDirectory } from './listDirectory.js';
|
|
|
7
7
|
import { duckDuckGo } from './duckDuckGo.js';
|
|
8
8
|
import { duckDuckGoScrape } from './duckDuckGoScrape.js';
|
|
9
9
|
import { patchFile } from './patchFile.js';
|
|
10
|
+
import { activateSkill } from './activateSkill.js';
|
|
10
11
|
|
|
11
12
|
export const TOOLS = [
|
|
12
13
|
{
|
|
@@ -133,6 +134,17 @@ export const TOOLS = [
|
|
|
133
134
|
},
|
|
134
135
|
required: ['filepath', 'edits']
|
|
135
136
|
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'activate_skill',
|
|
140
|
+
description: 'Activates a specialized agent skill by name. Returns the skill\'s instructions wrapped in <activated_skill> tags. These provide specialized guidance for the current task.',
|
|
141
|
+
parameters: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
skillName: { type: 'string', description: 'The name or ID of the skill to activate.' }
|
|
145
|
+
},
|
|
146
|
+
required: ['skillName']
|
|
147
|
+
}
|
|
136
148
|
}
|
|
137
149
|
];
|
|
138
150
|
|
|
@@ -156,6 +168,7 @@ export async function executeTool(name, args) {
|
|
|
156
168
|
case 'duck_duck_go': return await duckDuckGo(args);
|
|
157
169
|
case 'duck_duck_go_scrape': return await duckDuckGoScrape(args);
|
|
158
170
|
case 'patch_file': return await patchFile(args);
|
|
171
|
+
case 'activate_skill': return await activateSkill(args);
|
|
159
172
|
default: return `Unknown tool: ${name}`;
|
|
160
173
|
}
|
|
161
174
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { marked } from 'marked';
|
|
2
|
+
import { markedTerminal } from 'marked-terminal';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
marked.use(markedTerminal({
|
|
6
|
+
// Prominent headings
|
|
7
|
+
firstHeading: chalk.magenta.bold.underline,
|
|
8
|
+
heading: chalk.magenta.bold,
|
|
9
|
+
|
|
10
|
+
// Stronger emphasis for other elements
|
|
11
|
+
strong: chalk.yellow.bold,
|
|
12
|
+
em: chalk.italic,
|
|
13
|
+
codespan: chalk.bgRgb(40, 40, 40).yellow,
|
|
14
|
+
|
|
15
|
+
// Custom tab/padding
|
|
16
|
+
tab: 4
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
export function printMarkdown(text) {
|
|
20
|
+
process.stdout.write(marked.parse(text));
|
|
21
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const SKILLS_DIR = path.join(os.homedir(), '.config', 'banana-code', 'skills');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Scans the skills directory and parses SKILL.md files.
|
|
9
|
+
* @returns {Array} List of discovered skills.
|
|
10
|
+
*/
|
|
11
|
+
export function getAvailableSkills() {
|
|
12
|
+
try {
|
|
13
|
+
if (!fs.existsSync(SKILLS_DIR)) {
|
|
14
|
+
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let skills = [];
|
|
21
|
+
try {
|
|
22
|
+
const entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
const skillPath = path.join(SKILLS_DIR, entry.name);
|
|
26
|
+
const mdPath = path.join(skillPath, 'SKILL.md');
|
|
27
|
+
|
|
28
|
+
if (fs.existsSync(mdPath)) {
|
|
29
|
+
try {
|
|
30
|
+
const content = fs.readFileSync(mdPath, 'utf8');
|
|
31
|
+
// Match YAML frontmatter between --- and ---
|
|
32
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
33
|
+
|
|
34
|
+
if (match) {
|
|
35
|
+
const frontmatter = match[1];
|
|
36
|
+
const body = content.slice(match[0].length).trim();
|
|
37
|
+
|
|
38
|
+
// Simple YAML parsing via regex
|
|
39
|
+
const nameMatch = frontmatter.match(/name:\s*['"]?([^'"\n]+)['"]?/);
|
|
40
|
+
const descMatch = frontmatter.match(/description:\s*['"]?([^'"\n]+)['"]?/);
|
|
41
|
+
|
|
42
|
+
if (nameMatch && descMatch) {
|
|
43
|
+
skills.push({
|
|
44
|
+
id: entry.name,
|
|
45
|
+
name: nameMatch[1].trim(),
|
|
46
|
+
description: descMatch[1].trim(),
|
|
47
|
+
instructions: body,
|
|
48
|
+
path: skillPath
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
// Skip corrupted or unreadable skills
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {}
|
|
59
|
+
|
|
60
|
+
return skills;
|
|
61
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Estimates the number of tokens in a given string.
|
|
3
|
+
* This is a rough approximation (1 token ≈ 4 characters or ~0.75 words)
|
|
4
|
+
* used to provide a quick estimate without needing heavy, provider-specific tokenizer libraries.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} text - The input text to estimate tokens for.
|
|
7
|
+
* @returns {number} The estimated token count.
|
|
8
|
+
*/
|
|
9
|
+
export function estimateTokens(text) {
|
|
10
|
+
if (!text || typeof text !== 'string') return 0;
|
|
11
|
+
|
|
12
|
+
// A common heuristic: 1 token is roughly 4 English characters.
|
|
13
|
+
// For code, it can be denser, but this provides a reasonable ballpark.
|
|
14
|
+
return Math.ceil(text.length / 4);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Calculates the estimated token count for the entire conversation history.
|
|
19
|
+
*
|
|
20
|
+
* @param {Array} messages - The array of message objects.
|
|
21
|
+
* @returns {number} The estimated total tokens.
|
|
22
|
+
*/
|
|
23
|
+
export function estimateConversationTokens(messages) {
|
|
24
|
+
if (!Array.isArray(messages)) return 0;
|
|
25
|
+
|
|
26
|
+
let totalString = '';
|
|
27
|
+
|
|
28
|
+
// Stringify the entire message array to get a representation of its "weight"
|
|
29
|
+
// This includes system prompts, tool calls, and results.
|
|
30
|
+
try {
|
|
31
|
+
totalString = JSON.stringify(messages);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
// Fallback if there are circular references (unlikely in simple message arrays)
|
|
34
|
+
messages.forEach(msg => {
|
|
35
|
+
if (typeof msg === 'string') totalString += msg;
|
|
36
|
+
else if (msg && typeof msg === 'object') {
|
|
37
|
+
if (msg.content) totalString += typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
38
|
+
if (msg.parts) totalString += JSON.stringify(msg.parts);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return estimateTokens(totalString);
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
|
|
5
|
+
export async function getWorkspaceTree() {
|
|
6
|
+
let ignores = ['node_modules/**', '.git/**'];
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const gitignore = await fs.readFile(path.join(process.cwd(), '.gitignore'), 'utf8');
|
|
10
|
+
ignores = ignores.concat(gitignore.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')));
|
|
11
|
+
} catch (e) {}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const bananacodeignore = await fs.readFile(path.join(process.cwd(), '.bananacodeignore'), 'utf8');
|
|
15
|
+
ignores = ignores.concat(bananacodeignore.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')));
|
|
16
|
+
} catch (e) {}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const files = await glob('**/*', {
|
|
20
|
+
cwd: process.cwd(),
|
|
21
|
+
ignore: ignores,
|
|
22
|
+
nodir: true,
|
|
23
|
+
dot: true
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return files.join('\n');
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return `Error reading workspace: ${err.message}`;
|
|
29
|
+
}
|
|
30
|
+
}
|