@banaxi/banana-code 1.2.0 → 1.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banaxi/banana-code",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "🍌 BananaCode",
5
5
  "keywords": [
6
6
  "banana",
package/src/config.js CHANGED
@@ -6,7 +6,7 @@ import { execSync } from 'child_process';
6
6
  import fsSync from 'fs';
7
7
  import chalk from 'chalk';
8
8
 
9
- import { GEMINI_MODELS, CLAUDE_MODELS, OPENAI_MODELS, CODEX_MODELS, OLLAMA_CLOUD_MODELS } from './constants.js';
9
+ import { GEMINI_MODELS, CLAUDE_MODELS, OPENAI_MODELS, CODEX_MODELS, OLLAMA_CLOUD_MODELS, MISTRAL_MODELS } from './constants.js';
10
10
 
11
11
  const CONFIG_DIR = path.join(os.homedir(), '.config', 'banana-code');
12
12
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
@@ -74,6 +74,27 @@ export async function setupProvider(provider, config = {}) {
74
74
  message: 'Select a Claude model:',
75
75
  choices: CLAUDE_MODELS
76
76
  });
77
+ } else if (provider === 'mistral') {
78
+ config.apiKey = await input({
79
+ message: 'Enter your MISTRAL_API_KEY (from console.mistral.ai):',
80
+ default: config.apiKey
81
+ });
82
+
83
+ const choices = [...MISTRAL_MODELS, { name: chalk.magenta('✎ Enter custom model ID...'), value: 'CUSTOM_ID' }];
84
+ let selectedModel = await select({
85
+ message: 'Select a Mistral model:',
86
+ choices,
87
+ loop: false,
88
+ pageSize: Math.max(choices.length, 15)
89
+ });
90
+
91
+ if (selectedModel === 'CUSTOM_ID') {
92
+ selectedModel = await input({
93
+ message: 'Enter the exact Mistral model ID (e.g., mistral-large-latest):',
94
+ validate: (v) => v.trim().length > 0 || 'Model ID cannot be empty'
95
+ });
96
+ }
97
+ config.model = selectedModel;
77
98
  } else if (provider === 'openai') {
78
99
  const authMethod = await select({
79
100
  message: 'How would you like to authenticate with OpenAI?',
@@ -158,6 +179,7 @@ async function runSetupWizard() {
158
179
  { name: 'Google Gemini', value: 'gemini' },
159
180
  { name: 'Anthropic Claude', value: 'claude' },
160
181
  { name: 'OpenAI', value: 'openai' },
182
+ { name: 'Mistral AI', value: 'mistral' },
161
183
  { name: 'Ollama Cloud', value: 'ollama_cloud' },
162
184
  { name: 'Ollama (Local)', value: 'ollama' }
163
185
  ]
package/src/constants.js CHANGED
@@ -30,6 +30,15 @@ export const OLLAMA_CLOUD_MODELS = [
30
30
  { name: 'Llama 3.1 405B (Cloud)', value: 'llama3.1:405b-cloud' }
31
31
  ];
32
32
 
33
+ export const MISTRAL_MODELS = [
34
+ { name: 'Mistral Large (Latest)', value: 'mistral-large-latest' },
35
+ { name: 'Mistral Medium (Latest)', value: 'mistral-medium-latest' },
36
+ { name: 'Mistral Small (Latest)', value: 'mistral-small-latest' },
37
+ { name: 'Codestral (Latest)', value: 'codestral-latest' },
38
+ { name: 'Mistral Nemo', value: 'open-mistral-nemo' },
39
+ { name: 'Pixtral 12B', value: 'pixtral-12b-2409' }
40
+ ];
41
+
33
42
  export const CODEX_MODELS = [
34
43
  { name: 'GPT-5.4 (Newest)', value: 'gpt-5.4' },
35
44
  { name: 'GPT-5.3 Codex', value: 'gpt-5.3-codex' },
package/src/index.js CHANGED
@@ -10,6 +10,7 @@ import { ClaudeProvider } from './providers/claude.js';
10
10
  import { OpenAIProvider } from './providers/openai.js';
11
11
  import { OllamaProvider } from './providers/ollama.js';
12
12
  import { OllamaCloudProvider } from './providers/ollamaCloud.js';
13
+ import { MistralProvider } from './providers/mistral.js';
13
14
 
14
15
  import { loadSession, saveSession, generateSessionId, getLatestSessionId, listSessions } from './sessions.js';
15
16
  import { printMarkdown } from './utils/markdown.js';
@@ -28,6 +29,7 @@ function createProvider(overrideConfig = null) {
28
29
  case 'gemini': return new GeminiProvider(activeConfig);
29
30
  case 'claude': return new ClaudeProvider(activeConfig);
30
31
  case 'openai': return new OpenAIProvider(activeConfig);
32
+ case 'mistral': return new MistralProvider(activeConfig);
31
33
  case 'ollama_cloud': return new OllamaCloudProvider(activeConfig);
32
34
  case 'ollama': return new OllamaProvider(activeConfig);
33
35
  default:
@@ -51,20 +53,21 @@ async function handleSlashCommand(command) {
51
53
  { name: 'Google Gemini', value: 'gemini' },
52
54
  { name: 'Anthropic Claude', value: 'claude' },
53
55
  { name: 'OpenAI', value: 'openai' },
56
+ { name: 'Mistral AI', value: 'mistral' },
54
57
  { name: 'Ollama Cloud', value: 'ollama_cloud' },
55
58
  { name: 'Ollama (Local)', value: 'ollama' }
56
59
  ]
57
60
  });
58
61
  }
59
62
 
60
- if (['gemini', 'claude', 'openai', 'ollama_cloud', 'ollama'].includes(newProv)) {
63
+ if (['gemini', 'claude', 'openai', 'mistral', 'ollama_cloud', 'ollama'].includes(newProv)) {
61
64
  // Use the shared setup logic to get keys/models
62
65
  config = await setupProvider(newProv, config);
63
66
  await saveConfig(config);
64
67
  providerInstance = createProvider();
65
68
  console.log(chalk.green(`Switched provider to ${newProv} (${config.model}).`));
66
69
  } else {
67
- console.log(chalk.yellow(`Usage: /provider <gemini|claude|openai|ollama_cloud|ollama>`));
70
+ console.log(chalk.yellow(`Usage: /provider <gemini|claude|openai|mistral|ollama_cloud|ollama>`));
68
71
  }
69
72
  break;
70
73
  case '/model':
@@ -72,13 +75,15 @@ async function handleSlashCommand(command) {
72
75
  if (!newModel) {
73
76
  // Interactive selection
74
77
  const { select } = await import('@inquirer/prompts');
75
- const { GEMINI_MODELS, CLAUDE_MODELS, OPENAI_MODELS, CODEX_MODELS, OLLAMA_CLOUD_MODELS } = await import('./constants.js');
78
+ const { GEMINI_MODELS, CLAUDE_MODELS, OPENAI_MODELS, CODEX_MODELS, OLLAMA_CLOUD_MODELS, MISTRAL_MODELS } = await import('./constants.js');
76
79
 
77
80
  let choices = [];
78
81
  if (config.provider === 'gemini') choices = GEMINI_MODELS;
79
82
  else if (config.provider === 'claude') choices = CLAUDE_MODELS;
80
83
  else if (config.provider === 'openai') {
81
84
  choices = config.authType === 'oauth' ? CODEX_MODELS : OPENAI_MODELS;
85
+ } else if (config.provider === 'mistral') {
86
+ choices = MISTRAL_MODELS;
82
87
  } else if (config.provider === 'ollama_cloud') {
83
88
  choices = OLLAMA_CLOUD_MODELS;
84
89
  } else if (config.provider === 'ollama') {
@@ -94,7 +99,7 @@ async function handleSlashCommand(command) {
94
99
 
95
100
  if (choices.length > 0) {
96
101
  const finalChoices = [...choices];
97
- if (config.provider === 'ollama_cloud') {
102
+ if (config.provider === 'ollama_cloud' || config.provider === 'mistral') {
98
103
  finalChoices.push({ name: chalk.magenta('✎ Enter custom model ID...'), value: 'CUSTOM_ID' });
99
104
  }
100
105
 
@@ -381,7 +386,7 @@ async function handleSlashCommand(command) {
381
386
  case '/help':
382
387
  console.log(chalk.yellow(`
383
388
  Available commands:
384
- /provider <name> - Switch AI provider (gemini, claude, openai, ollama_cloud, ollama)
389
+ /provider <name> - Switch AI provider (gemini, claude, openai, mistral, ollama_cloud, ollama)
385
390
  /model [name] - Switch model within current provider (opens menu if name omitted)
386
391
  /chats - List persistent chat sessions
387
392
  /clear - Clear chat history
package/src/prompt.js CHANGED
@@ -37,6 +37,19 @@ Always use tools when they would help. Be concise but thorough. `;
37
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
38
  }
39
39
 
40
+ const hasDelegateTool = availableToolsList.some(t => t.name === 'delegate_task');
41
+ if (hasDelegateTool) {
42
+ prompt += `
43
+ \n# Sub-Agent Delegation
44
+ You have the ability to spawn specialized sub-agents to handle complex sub-tasks using the \`delegate_task\` tool.
45
+ - Use **researcher** for deep codebase exploration or fact-finding.
46
+ - Use **coder** for implementing specific features or complex bug fixes.
47
+ - Use **reviewer** for analyzing code quality or security.
48
+ - Use **generalist** for any other multi-step sub-task.
49
+ Delegation is highly recommended for tasks that would otherwise bloat your current conversation context. The results of the sub-agent will be returned to you as a summary.
50
+ `;
51
+ }
52
+
40
53
  if (config.planMode) {
41
54
  prompt += `
42
55
  [PLAN MODE ENABLED]
@@ -114,7 +114,7 @@ export class ClaudeProvider {
114
114
  const toolResultContent = [];
115
115
  for (const call of toolCalls) {
116
116
  console.log(chalk.yellow(`\n[Banana Calling Tool: ${call.name}]`));
117
- const res = await executeTool(call.name, call.input);
117
+ const res = await executeTool(call.name, call.input, this.config);
118
118
  if (this.config.debug) {
119
119
  console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
120
120
  }
@@ -135,7 +135,7 @@ export class GeminiProvider {
135
135
  hasToolCalls = true;
136
136
  const call = part.functionCall;
137
137
  console.log(chalk.yellow(`\n[Banana Calling Tool: ${call.name}]`));
138
- const res = await executeTool(call.name, call.args);
138
+ const res = await executeTool(call.name, call.args, this.config);
139
139
  if (this.config.debug) {
140
140
  console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
141
141
  }
@@ -0,0 +1,141 @@
1
+ import OpenAI from 'openai';
2
+ import { getAvailableTools, executeTool } from '../tools/registry.js';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { getSystemPrompt } from '../prompt.js';
6
+ import { printMarkdown } from '../utils/markdown.js';
7
+
8
+ export class MistralProvider {
9
+ constructor(config) {
10
+ this.config = config;
11
+ this.openai = new OpenAI({
12
+ apiKey: config.apiKey,
13
+ baseURL: 'https://api.mistral.ai/v1'
14
+ });
15
+ this.modelName = config.model || 'mistral-large-latest';
16
+ this.systemPrompt = getSystemPrompt(config);
17
+ this.messages = [{ role: 'system', content: this.systemPrompt }];
18
+ this.tools = getAvailableTools(config).map(t => ({
19
+ type: 'function',
20
+ function: {
21
+ name: t.name,
22
+ description: t.description,
23
+ parameters: t.parameters
24
+ }
25
+ }));
26
+ }
27
+
28
+ updateSystemPrompt(newPrompt) {
29
+ this.systemPrompt = newPrompt;
30
+ if (this.messages.length > 0 && this.messages[0].role === 'system') {
31
+ this.messages[0].content = newPrompt;
32
+ }
33
+ }
34
+
35
+ async sendMessage(message) {
36
+ this.messages.push({ role: 'user', content: message });
37
+
38
+ let spinner = ora({ text: 'Thinking...', color: 'yellow', stream: process.stdout }).start();
39
+ let finalResponse = '';
40
+
41
+ try {
42
+ while (true) {
43
+ let stream = null;
44
+ try {
45
+ stream = await this.openai.chat.completions.create({
46
+ model: this.modelName,
47
+ messages: this.messages,
48
+ tools: this.tools.length > 0 ? this.tools : undefined,
49
+ stream: true
50
+ });
51
+ } catch (e) {
52
+ spinner.stop();
53
+ console.error(chalk.red(`Mistral Request Error: ${e.message}`));
54
+ return `Error: ${e.message}`;
55
+ }
56
+
57
+ let chunkResponse = '';
58
+ let toolCalls = [];
59
+
60
+ for await (const chunk of stream) {
61
+ const delta = chunk.choices[0]?.delta;
62
+
63
+ if (delta?.content) {
64
+ if (spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
65
+ if (!this.config.useMarkedTerminal) {
66
+ process.stdout.write(chalk.cyan(delta.content));
67
+ }
68
+ chunkResponse += delta.content;
69
+ finalResponse += delta.content;
70
+ }
71
+
72
+ if (delta?.tool_calls) {
73
+ if (spinner.isSpinning) spinner.stop();
74
+ for (const tc of delta.tool_calls) {
75
+ if (tc.index === undefined) continue;
76
+ if (!toolCalls[tc.index]) {
77
+ toolCalls[tc.index] = { id: tc.id, type: 'function', function: { name: tc.function.name, arguments: '' } };
78
+ }
79
+ if (tc.function?.arguments) {
80
+ toolCalls[tc.index].function.arguments += tc.function.arguments;
81
+ // Visual feedback for streaming tool arguments
82
+ if (!spinner.isSpinning) {
83
+ spinner = ora({ text: `Generating ${chalk.yellow(toolCalls[tc.index].function.name)} (${toolCalls[tc.index].function.arguments.length} bytes)...`, color: 'yellow', stream: process.stdout }).start();
84
+ } else {
85
+ spinner.text = `Generating ${chalk.yellow(toolCalls[tc.index].function.name)} (${toolCalls[tc.index].function.arguments.length} bytes)...`;
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ if (spinner.isSpinning) spinner.stop();
92
+
93
+ if (chunkResponse) {
94
+ if (this.config.useMarkedTerminal) printMarkdown(chunkResponse);
95
+ this.messages.push({ role: 'assistant', content: chunkResponse });
96
+ }
97
+
98
+ toolCalls = toolCalls.filter(Boolean);
99
+
100
+ if (toolCalls.length === 0) {
101
+ console.log();
102
+ break;
103
+ }
104
+
105
+ this.messages.push({
106
+ role: 'assistant',
107
+ tool_calls: toolCalls,
108
+ content: chunkResponse || null
109
+ });
110
+
111
+ for (const call of toolCalls) {
112
+ console.log(chalk.yellow(`\n[Banana Calling Tool: ${call.function.name}]`));
113
+ let args = {};
114
+ try {
115
+ args = JSON.parse(call.function.arguments);
116
+ } catch (e) { }
117
+
118
+ const res = await executeTool(call.function.name, args, this.config);
119
+ if (this.config.debug) {
120
+ console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
121
+ }
122
+ console.log(chalk.yellow(`[Tool Result Received]\n`));
123
+
124
+ this.messages.push({
125
+ role: 'tool',
126
+ tool_call_id: call.id,
127
+ content: typeof res === 'string' ? res : JSON.stringify(res)
128
+ });
129
+ }
130
+
131
+ spinner = ora({ text: 'Processing tool results...', color: 'yellow', stream: process.stdout }).start();
132
+ }
133
+
134
+ return finalResponse;
135
+ } catch (err) {
136
+ if (spinner && spinner.isSpinning) spinner.stop();
137
+ console.error(chalk.red(`Mistral Runtime Error: ${err.message}`));
138
+ return `Error: ${err.message}`;
139
+ }
140
+ }
141
+ }
@@ -47,7 +47,7 @@ export class OllamaCloudProvider {
47
47
  model: this.modelName,
48
48
  messages: this.messages,
49
49
  tools: this.tools.length > 0 ? this.tools : undefined,
50
- stream: false // Cloud API sometimes prefers non-streaming or different SSE formats
50
+ stream: true
51
51
  })
52
52
  });
53
53
 
@@ -57,32 +57,61 @@ export class OllamaCloudProvider {
57
57
  throw new Error(`HTTP ${response.status}: ${errorText}`);
58
58
  }
59
59
 
60
- const data = await response.json();
61
- spinner.stop();
60
+ const reader = response.body.getReader();
61
+ const decoder = new TextDecoder();
62
+ let currentChunkResponse = '';
63
+ let lastMessageObj = null;
64
+
65
+ while (true) {
66
+ const { done, value } = await reader.read();
67
+ if (done) break;
68
+
69
+ const chunk = decoder.decode(value, { stream: true });
70
+ const lines = chunk.split('\n').filter(l => l.trim() !== '');
71
+
72
+ for (const line of lines) {
73
+ try {
74
+ const data = JSON.parse(line);
75
+ if (data.message) {
76
+ lastMessageObj = data.message;
77
+ if (data.message.content) {
78
+ const content = data.message.content;
79
+ if (spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
80
+ if (!this.config.useMarkedTerminal) {
81
+ process.stdout.write(chalk.cyan(content));
82
+ }
83
+ currentChunkResponse += content;
84
+ finalResponse += content;
85
+ }
86
+ }
87
+ } catch (e) {
88
+ // Ignore partial JSON chunks
89
+ }
90
+ }
91
+ }
62
92
 
63
- const messageObj = data.message;
93
+ if (spinner.isSpinning) spinner.stop();
64
94
 
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;
95
+ if (currentChunkResponse && this.config.useMarkedTerminal) {
96
+ printMarkdown(currentChunkResponse);
97
+ }
98
+
99
+ if (!lastMessageObj) {
100
+ throw new Error("Empty response from Ollama Cloud");
72
101
  }
73
102
 
74
- this.messages.push(messageObj);
103
+ this.messages.push(lastMessageObj);
75
104
 
76
- if (!messageObj.tool_calls || messageObj.tool_calls.length === 0) {
105
+ if (!lastMessageObj.tool_calls || lastMessageObj.tool_calls.length === 0) {
77
106
  console.log();
78
107
  break;
79
108
  }
80
109
 
81
- for (const call of messageObj.tool_calls) {
110
+ for (const call of lastMessageObj.tool_calls) {
82
111
  const fn = call.function;
83
112
  console.log(chalk.yellow(`\n[Banana Calling Tool: ${fn.name}]`));
84
113
 
85
- let res = await executeTool(fn.name, fn.arguments);
114
+ let res = await executeTool(fn.name, fn.arguments, this.config);
86
115
  if (this.config.debug) {
87
116
  console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
88
117
  }
@@ -121,7 +121,7 @@ export class OpenAIProvider {
121
121
  args = JSON.parse(call.function.arguments);
122
122
  } catch (e) { }
123
123
 
124
- const res = await executeTool(call.function.name, args);
124
+ const res = await executeTool(call.function.name, args, this.config);
125
125
  if (this.config.debug) {
126
126
  console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
127
127
  }
@@ -371,7 +371,7 @@ export class OpenAIProvider {
371
371
  args = JSON.parse(call.function.arguments);
372
372
  } catch (e) { }
373
373
 
374
- const res = await executeTool(call.function.name, args);
374
+ const res = await executeTool(call.function.name, args, this.config);
375
375
  if (this.config.debug) {
376
376
  console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
377
377
  }
@@ -0,0 +1,100 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getAvailableTools, executeTool } from './registry.js';
4
+ import { getSystemPrompt } from '../prompt.js';
5
+ import { requestPermission } from '../permissions.js';
6
+
7
+ // Specialist prompts to guide sub-agents
8
+ const SPECIALIST_PROMPTS = {
9
+ researcher: "You are a Research Specialist. Your goal is to explore the codebase, find information, and answer specific questions. Do not make any file changes. Use tools like search_files, list_directory, and read_file to gather facts.",
10
+ coder: "You are a Coding Specialist. Your goal is to implement specific logic or fix bugs as requested. Focus on writing high-quality, idiomatic code using patch_file and write_file.",
11
+ reviewer: "You are a Code Reviewer. Your goal is to analyze provided code for bugs, security vulnerabilities, or style issues. Provide a detailed report of your findings.",
12
+ generalist: "You are a Generalist Sub-Agent. Complete the assigned task as efficiently as possible using all available tools."
13
+ };
14
+
15
+ /**
16
+ * Tool that allows the main agent to delegate a sub-task to a specialized agent.
17
+ */
18
+ export async function delegateTask({ task, agentType = 'generalist', contextFiles = [] }, mainConfig) {
19
+ const perm = await requestPermission('Delegate Task', `${agentType} specialist: ${task}`);
20
+ if (!perm.allowed) {
21
+ return `User denied permission to delegate task to ${agentType} specialist.`;
22
+ }
23
+
24
+ const spinner = ora({
25
+ text: `Delegating to ${chalk.magenta(agentType)} specialist...`,
26
+ color: 'magenta',
27
+ stream: process.stdout
28
+ }).start();
29
+
30
+ try {
31
+ // 1. Setup the sub-agent config (inherit from main, but could be customized)
32
+ const subConfig = { ...mainConfig };
33
+
34
+ // Use a dynamic import to avoid circular dependency with index.js if needed,
35
+ // but here we can just manually create the provider based on current config.
36
+ const { GeminiProvider } = await import('../providers/gemini.js');
37
+ const { ClaudeProvider } = await import('../providers/claude.js');
38
+ const { OpenAIProvider } = await import('../providers/openai.js');
39
+ const { MistralProvider } = await import('../providers/mistral.js');
40
+ const { OllamaProvider } = await import('../providers/ollama.js');
41
+ const { OllamaCloudProvider } = await import('../providers/ollamaCloud.js');
42
+
43
+ const createSubProvider = (cfg) => {
44
+ switch (cfg.provider) {
45
+ case 'gemini': return new GeminiProvider(cfg);
46
+ case 'claude': return new ClaudeProvider(cfg);
47
+ case 'openai': return new OpenAIProvider(cfg);
48
+ case 'mistral': return new MistralProvider(cfg);
49
+ case 'ollama_cloud': return new OllamaCloudProvider(cfg);
50
+ case 'ollama': return new OllamaProvider(cfg);
51
+ default: return new OllamaProvider(cfg);
52
+ }
53
+ };
54
+
55
+ const subProvider = createSubProvider(subConfig);
56
+
57
+ // 2. Customize the system prompt for the specialist
58
+ const basePrompt = getSystemPrompt(subConfig);
59
+ const specialistInstruction = SPECIALIST_PROMPTS[agentType] || SPECIALIST_PROMPTS.generalist;
60
+
61
+ // Inject specialist instructions at the start
62
+ subProvider.updateSystemPrompt(`${specialistInstruction}\n\n${basePrompt}`);
63
+
64
+ // 3. Prepare initial message with context
65
+ let initialMessage = `TASK: ${task}`;
66
+ if (contextFiles.length > 0) {
67
+ const fs = await import('fs');
68
+ initialMessage += "\n\nCONTEXT FILES:";
69
+ for (const file of contextFiles) {
70
+ try {
71
+ const content = fs.readFileSync(file, 'utf8');
72
+ initialMessage += `\n\n--- ${file} ---\n${content}`;
73
+ } catch (e) {
74
+ initialMessage += `\n\n(Error reading context file ${file}: ${e.message})`;
75
+ }
76
+ }
77
+ }
78
+
79
+ // 4. Run the sub-agent message loop
80
+ // We'll give it a limit of 5 turns to prevent infinite loops between agents
81
+ let turns = 0;
82
+ let finalResponse = '';
83
+
84
+ spinner.text = `Sub-agent (${agentType}) is working on the task...`;
85
+
86
+ // For the sub-agent, we want to capture its sendMessage result
87
+ // Note: Sub-agents run silently (their output isn't printed unless debug is on)
88
+ // to prevent terminal clutter.
89
+ finalResponse = await subProvider.sendMessage(initialMessage);
90
+
91
+ spinner.stop();
92
+ console.log(chalk.magenta(`[Sub-Agent ${agentType} task complete]`));
93
+
94
+ return `SUB-AGENT RESULT:\n${finalResponse}`;
95
+
96
+ } catch (err) {
97
+ if (spinner.isSpinning) spinner.stop();
98
+ return `Error in delegation: ${err.message}`;
99
+ }
100
+ }
@@ -8,6 +8,7 @@ import { duckDuckGo } from './duckDuckGo.js';
8
8
  import { duckDuckGoScrape } from './duckDuckGoScrape.js';
9
9
  import { patchFile } from './patchFile.js';
10
10
  import { activateSkill } from './activateSkill.js';
11
+ import { delegateTask } from './delegateTask.js';
11
12
 
12
13
  export const TOOLS = [
13
14
  {
@@ -145,6 +146,29 @@ export const TOOLS = [
145
146
  },
146
147
  required: ['skillName']
147
148
  }
149
+ },
150
+ {
151
+ name: 'delegate_task',
152
+ label: 'Sub-Agent Delegation (Beta)',
153
+ description: 'Spawns a specialized sub-agent to handle a specific sub-task. Use this for complex research, big code changes, or detailed reviews to keep the main context clean.',
154
+ beta: true,
155
+ parameters: {
156
+ type: 'object',
157
+ properties: {
158
+ task: { type: 'string', description: 'The specific, detailed instruction for the sub-agent.' },
159
+ agentType: {
160
+ type: 'string',
161
+ description: 'The type of specialist to spawn.',
162
+ enum: ['researcher', 'coder', 'reviewer', 'generalist']
163
+ },
164
+ contextFiles: {
165
+ type: 'array',
166
+ description: 'Optional list of file paths to provide as initial context to the sub-agent.',
167
+ items: { type: 'string' }
168
+ }
169
+ },
170
+ required: ['task']
171
+ }
148
172
  }
149
173
  ];
150
174
 
@@ -157,7 +181,7 @@ export function getAvailableTools(config = {}) {
157
181
  });
158
182
  }
159
183
 
160
- export async function executeTool(name, args) {
184
+ export async function executeTool(name, args, config) {
161
185
  switch (name) {
162
186
  case 'execute_command': return await execCommand(args);
163
187
  case 'read_file': return await readFile(args);
@@ -169,6 +193,7 @@ export async function executeTool(name, args) {
169
193
  case 'duck_duck_go_scrape': return await duckDuckGoScrape(args);
170
194
  case 'patch_file': return await patchFile(args);
171
195
  case 'activate_skill': return await activateSkill(args);
196
+ case 'delegate_task': return await delegateTask(args, config);
172
197
  default: return `Unknown tool: ${name}`;
173
198
  }
174
199
  }