@banaxi/banana-code 1.0.4 → 1.1.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.
@@ -1,18 +1,33 @@
1
- import { TOOLS, executeTool } from '../tools/registry.js';
1
+ 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
-
6
- const SYSTEM_PROMPT = getSystemPrompt();
5
+ import { printMarkdown } from '../utils/markdown.js';
7
6
 
8
7
  export class OllamaProvider {
9
8
  constructor(config) {
9
+ this.config = config;
10
10
  this.modelName = config.model || 'llama3';
11
- this.messages = [{ role: 'system', content: SYSTEM_PROMPT }];
12
- this.tools = TOOLS.map(t => ({ type: 'function', function: t }));
11
+ this.systemPrompt = getSystemPrompt(config);
12
+ this.messages = [{ role: 'system', content: this.systemPrompt }];
13
+ this.tools = getAvailableTools(config).map(t => ({
14
+ type: 'function',
15
+ function: {
16
+ name: t.name,
17
+ description: t.description,
18
+ parameters: t.parameters
19
+ }
20
+ }));
13
21
  this.URL = 'http://localhost:11434/api/chat';
14
22
  }
15
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
+
16
31
  async sendMessage(message) {
17
32
  this.messages.push({ role: 'user', content: message });
18
33
 
@@ -42,7 +57,11 @@ export class OllamaProvider {
42
57
  const messageObj = data.message;
43
58
 
44
59
  if (messageObj.content) {
45
- process.stdout.write(chalk.cyan(messageObj.content));
60
+ if (this.config.useMarkedTerminal) {
61
+ printMarkdown(messageObj.content);
62
+ } else {
63
+ process.stdout.write(chalk.cyan(messageObj.content));
64
+ }
46
65
  finalResponse += messageObj.content;
47
66
  }
48
67
 
@@ -58,6 +77,9 @@ export class OllamaProvider {
58
77
  console.log(chalk.yellow(`\n[Banana Calling Tool: ${fn.name}]`));
59
78
 
60
79
  let res = await executeTool(fn.name, fn.arguments);
80
+ if (this.config.debug) {
81
+ console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
82
+ }
61
83
  console.log(chalk.yellow(`[Tool Result Received]\n`));
62
84
 
63
85
  this.messages.push({
@@ -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
+ }
@@ -1,13 +1,12 @@
1
1
  import OpenAI from 'openai';
2
- import { TOOLS, executeTool } from '../tools/registry.js';
2
+ import { getAvailableTools, executeTool } from '../tools/registry.js';
3
3
  import chalk from 'chalk';
4
4
  import ora from 'ora';
5
5
  import os from 'os';
6
6
  import path from 'path';
7
7
  import fsSync from 'fs';
8
8
  import { getSystemPrompt } from '../prompt.js';
9
-
10
- const SYSTEM_PROMPT = getSystemPrompt();
9
+ import { printMarkdown } from '../utils/markdown.js';
11
10
 
12
11
  export class OpenAIProvider {
13
12
  constructor(config) {
@@ -16,8 +15,23 @@ export class OpenAIProvider {
16
15
  this.openai = new OpenAI({ apiKey: config.apiKey });
17
16
  }
18
17
  this.modelName = config.model || 'gpt-4o';
19
- this.messages = [{ role: 'system', content: SYSTEM_PROMPT }];
20
- this.tools = TOOLS.map(t => ({ type: 'function', function: t }));
18
+ this.systemPrompt = getSystemPrompt(config);
19
+ this.messages = [{ role: 'system', content: this.systemPrompt }];
20
+ this.tools = getAvailableTools(config).map(t => ({
21
+ type: 'function',
22
+ function: {
23
+ name: t.name,
24
+ description: t.description,
25
+ parameters: t.parameters
26
+ }
27
+ }));
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
+ }
21
35
  }
22
36
 
23
37
  async sendMessage(message) {
@@ -53,8 +67,10 @@ export class OpenAIProvider {
53
67
  const delta = chunk.choices[0]?.delta;
54
68
 
55
69
  if (delta?.content) {
56
- if (spinner.isSpinning) spinner.stop();
57
- process.stdout.write(chalk.cyan(delta.content));
70
+ if (spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
71
+ if (!this.config.useMarkedTerminal) {
72
+ process.stdout.write(chalk.cyan(delta.content));
73
+ }
58
74
  chunkResponse += delta.content;
59
75
  finalResponse += delta.content;
60
76
  }
@@ -81,6 +97,7 @@ export class OpenAIProvider {
81
97
  if (spinner.isSpinning) spinner.stop();
82
98
 
83
99
  if (chunkResponse) {
100
+ if (this.config.useMarkedTerminal) printMarkdown(chunkResponse);
84
101
  this.messages.push({ role: 'assistant', content: chunkResponse });
85
102
  }
86
103
 
@@ -105,6 +122,9 @@ export class OpenAIProvider {
105
122
  } catch (e) { }
106
123
 
107
124
  const res = await executeTool(call.function.name, args);
125
+ if (this.config.debug) {
126
+ console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
127
+ }
108
128
  console.log(chalk.yellow(`[Tool Result Received]\n`));
109
129
 
110
130
  this.messages.push({
@@ -197,6 +217,18 @@ export class OpenAIProvider {
197
217
  const backendInput = mapMessagesToBackend(this.messages);
198
218
  const backendTools = mapToolsToBackend(this.tools);
199
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
+
200
232
  const response = await fetch('https://chatgpt.com/backend-api/codex/responses', {
201
233
  method: 'POST',
202
234
  headers: {
@@ -207,17 +239,7 @@ export class OpenAIProvider {
207
239
  'originator': 'codex_cli_rs',
208
240
  'Accept': 'text/event-stream'
209
241
  },
210
- body: JSON.stringify({
211
- model: this.modelName || 'gpt-5.1-codex',
212
- instructions: SYSTEM_PROMPT,
213
- input: backendInput,
214
- tools: backendTools,
215
- store: false,
216
- stream: true,
217
- include: ["reasoning.encrypted_content"],
218
- reasoning: { effort: "medium", summary: "auto" },
219
- text: { verbosity: "medium" }
220
- })
242
+ body: JSON.stringify(payload)
221
243
  });
222
244
 
223
245
  if (!response.ok) {
@@ -250,10 +272,17 @@ export class OpenAIProvider {
250
272
  try {
251
273
  const data = JSON.parse(currentDataBuffer);
252
274
  if (currentEvent === 'response.output_text.delta') {
253
- if (spinner && spinner.isSpinning) spinner.stop();
254
- process.stdout.write(chalk.cyan(data.delta));
275
+ if (spinner && spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
276
+ if (!this.config.useMarkedTerminal) {
277
+ process.stdout.write(chalk.cyan(data.delta));
278
+ }
255
279
  currentChunkResponse += data.delta;
256
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
+ }
257
286
  } else if (currentEvent === 'response.output_item.added' && data.item?.type === 'function_call') {
258
287
  if (spinner && spinner.isSpinning) spinner.stop();
259
288
  currentToolCall = {
@@ -267,7 +296,7 @@ export class OpenAIProvider {
267
296
  if (!spinner.isSpinning) {
268
297
  spinner = ora({ text: `Generating ${chalk.yellow(currentToolCall.name)} (${currentToolCall.arguments.length} bytes)...`, color: 'yellow', stream: process.stdout }).start();
269
298
  } else {
270
- spinner.text = `Generating ${chalk.yellow(currentToolCall.name)} (${currentToolCall.arguments.length} bytes)...`;
299
+ spinner.text = `Generating ${chalk.yellow(currentToolCall.name)} arguments (${currentToolCall.arguments.length} bytes)...`;
271
300
  }
272
301
  } else if (currentEvent === 'response.output_item.done' && data.item?.type === 'function_call' && currentToolCall) {
273
302
  if (spinner && spinner.isSpinning) spinner.stop();
@@ -313,7 +342,6 @@ export class OpenAIProvider {
313
342
  currentChunkResponse += data.delta;
314
343
  finalResponse += data.delta;
315
344
  }
316
- // Note: tool calls usually don't end exactly at stream end without \n\n
317
345
  } catch (e) { }
318
346
  }
319
347
  }
@@ -321,6 +349,7 @@ export class OpenAIProvider {
321
349
  if (spinner.isSpinning) spinner.stop();
322
350
 
323
351
  if (currentChunkResponse) {
352
+ if (this.config.useMarkedTerminal) printMarkdown(currentChunkResponse);
324
353
  this.messages.push({ role: 'assistant', content: currentChunkResponse });
325
354
  }
326
355
 
@@ -343,6 +372,9 @@ export class OpenAIProvider {
343
372
  } catch (e) { }
344
373
 
345
374
  const res = await executeTool(call.function.name, args);
375
+ if (this.config.debug) {
376
+ console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
377
+ }
346
378
  console.log(chalk.yellow(`[Tool Result Received]\n`));
347
379
 
348
380
  this.messages.push({
@@ -0,0 +1,34 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+
4
+ export async function duckDuckGo({ query }) {
5
+ const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&pretty=1`;
6
+ const spinner = ora({ text: `Searching DuckDuckGo for ${chalk.cyan(query)}...`, color: 'yellow', stream: process.stdout }).start();
7
+
8
+ try {
9
+ const res = await fetch(url);
10
+ const data = await res.json();
11
+ if (spinner.isSpinning) spinner.stop();
12
+
13
+ // DuckDuckGo API returns a lot of fields, let's extract the most useful ones
14
+ const result = {
15
+ Abstract: data.Abstract,
16
+ AbstractText: data.AbstractText,
17
+ AbstractSource: data.AbstractSource,
18
+ AbstractURL: data.AbstractURL,
19
+ Answer: data.Answer,
20
+ Definition: data.Definition,
21
+ DefinitionSource: data.DefinitionSource,
22
+ DefinitionURL: data.DefinitionURL,
23
+ RelatedTopics: data.RelatedTopics?.slice(0, 5).map(topic => ({
24
+ Text: topic.Text,
25
+ FirstURL: topic.FirstURL
26
+ }))
27
+ };
28
+
29
+ return JSON.stringify(result, null, 2);
30
+ } catch (err) {
31
+ if (spinner.isSpinning) spinner.stop();
32
+ return `Error searching DuckDuckGo: ${err.message}`;
33
+ }
34
+ }
@@ -0,0 +1,93 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+
4
+ export async function duckDuckGoScrape({ query }) {
5
+ const url = `https://duckduckgo.com/lite/?q=${encodeURIComponent(query)}`;
6
+ const userAgent = 'BananaCode/1.0 (AI-Agent)';
7
+
8
+ const spinner = ora({ text: `Scraping DuckDuckGo Lite for ${chalk.cyan(query)}...`, color: 'yellow', stream: process.stdout }).start();
9
+
10
+ try {
11
+ const res = await fetch(url, {
12
+ headers: {
13
+ 'User-Agent': userAgent
14
+ }
15
+ });
16
+
17
+ if (res.status === 403 || res.status === 429) {
18
+ if (spinner.isSpinning) spinner.stop();
19
+ return "ERROR: We are currently Rate Limited by DuckDuckGo. Please try again later or use the Quick Answer tool.";
20
+ }
21
+
22
+ if (!res.ok) {
23
+ if (spinner.isSpinning) spinner.stop();
24
+ return `Error: DuckDuckGo returned HTTP ${res.status}`;
25
+ }
26
+
27
+ const html = await res.text();
28
+ if (spinner.isSpinning) spinner.stop();
29
+
30
+ // Basic "scraping" using regex since we don't have a full DOM parser like JSDOM here
31
+ // DuckDuckGo Lite results are usually in <a class="result-link" href="...">...</a>
32
+ // and have a titles in them.
33
+
34
+ const results = [];
35
+ // More flexible regex to find result-link tags
36
+ const tagRegex = /<a\s+[^>]*class=['"]result-link['"][^>]*>([\s\S]*?)<\/a>/gi;
37
+
38
+ let match;
39
+ while ((match = tagRegex.exec(html)) !== null) {
40
+ const fullTag = match[0];
41
+ const title = match[1].replace(/<[^>]*>/g, '').trim();
42
+ const hrefMatch = /href=['"]([^'"]+)['"]/i.exec(fullTag);
43
+
44
+ if (hrefMatch) {
45
+ let link = hrefMatch[1];
46
+
47
+ // Extract the actual URL from the uddg parameter if present
48
+ if (link.includes('uddg=')) {
49
+ // Extract using manual split if URLSearchParams is finicky with partials
50
+ const uddgPart = link.split('uddg=')[1]?.split('&')[0];
51
+ if (uddgPart) {
52
+ try {
53
+ link = decodeURIComponent(uddgPart);
54
+ } catch (e) { }
55
+ }
56
+ }
57
+
58
+ // Ensure link is absolute
59
+ if (link.startsWith('//')) {
60
+ link = 'https:' + link;
61
+ } else if (link.startsWith('/')) {
62
+ link = 'https://duckduckgo.com' + link;
63
+ }
64
+
65
+ results.push({ title, link });
66
+ }
67
+ }
68
+
69
+ if (results.length === 0) {
70
+ // Try alternative regex if the first one fails (lite structure can change)
71
+ const altRegex = /<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
72
+ let altMatch;
73
+ let count = 0;
74
+ while ((altMatch = altRegex.exec(html)) !== null && count < 10) {
75
+ const link = altMatch[1];
76
+ if (link.startsWith('http') && !link.includes('duckduckgo.com')) {
77
+ const title = altMatch[2].replace(/<[^>]*>/g, '').trim();
78
+ results.push({ title, link });
79
+ count++;
80
+ }
81
+ }
82
+ }
83
+
84
+ if (results.length === 0) {
85
+ return "No results found on DuckDuckGo Lite.";
86
+ }
87
+
88
+ return JSON.stringify(results.slice(0, 10), null, 2);
89
+ } catch (err) {
90
+ if (spinner.isSpinning) spinner.stop();
91
+ return `Error scraping DuckDuckGo: ${err.message}`;
92
+ }
93
+ }
@@ -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
- resolve(`Command exited with code ${code}.\nOutput:\n${output}`);
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
  }
@@ -0,0 +1,63 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { requestPermission } from '../permissions.js';
4
+ import * as diff from 'diff';
5
+ import chalk from 'chalk';
6
+
7
+ export async function patchFile({ filepath, edits }) {
8
+ const absPath = path.resolve(process.cwd(), filepath);
9
+
10
+ let content = '';
11
+ try {
12
+ content = await fs.readFile(absPath, 'utf8');
13
+ } catch (err) {
14
+ return `Error reading file: ${err.message}`;
15
+ }
16
+
17
+ const originalContent = content;
18
+
19
+ for (let i = 0; i < edits.length; i++) {
20
+ const { oldText, newText, occurrence = 1 } = edits[i];
21
+
22
+ if (!content.includes(oldText)) {
23
+ return `Edit ${i + 1} failed: Cannot find the exact text to replace in ${filepath}. Please ensure you provide the exact text including whitespace and indentation.`;
24
+ }
25
+
26
+ if (occurrence === 0) {
27
+ content = content.split(oldText).join(newText);
28
+ } else {
29
+ let index = -1;
30
+ for (let j = 0; j < occurrence; j++) {
31
+ index = content.indexOf(oldText, index + 1);
32
+ if (index === -1) {
33
+ return `Edit ${i + 1} failed: Cannot find occurrence ${occurrence} of the text to replace in ${filepath}.`;
34
+ }
35
+ }
36
+ content = content.substring(0, index) + newText + content.substring(index + oldText.length);
37
+ }
38
+ }
39
+
40
+ if (originalContent === content) {
41
+ return `No changes were made. The file is identical to the requested edit.`;
42
+ }
43
+
44
+ const patch = diff.createPatch(filepath, originalContent, content);
45
+
46
+ console.log(chalk.cyan(`\nPreviewing changes for ${filepath}:`));
47
+ patch.split('\n').filter(l => l.length > 0 && !l.startsWith('===') && !l.startsWith('---') && !l.startsWith('+++')).forEach(line => {
48
+ if (line.startsWith('+')) console.log(chalk.green(line));
49
+ else if (line.startsWith('-')) console.log(chalk.red(line));
50
+ else console.log(chalk.gray(line));
51
+ });
52
+ console.log('');
53
+
54
+ const perm = await requestPermission('Patch File', filepath);
55
+ if (!perm.allowed) return `User denied permission to patch: ${filepath}`;
56
+
57
+ try {
58
+ await fs.writeFile(absPath, content, 'utf8');
59
+ return `Successfully patched ${filepath}`;
60
+ } catch (err) {
61
+ return `Error writing file: ${err.message}`;
62
+ }
63
+ }
@@ -4,6 +4,9 @@ import { writeFile } from './writeFile.js';
4
4
  import { fetchUrl } from './fetchUrl.js';
5
5
  import { searchFiles } from './searchFiles.js';
6
6
  import { listDirectory } from './listDirectory.js';
7
+ import { duckDuckGo } from './duckDuckGo.js';
8
+ import { duckDuckGoScrape } from './duckDuckGoScrape.js';
9
+ import { patchFile } from './patchFile.js';
7
10
 
8
11
  export const TOOLS = [
9
12
  {
@@ -74,9 +77,74 @@ export const TOOLS = [
74
77
  },
75
78
  required: ['directoryPath']
76
79
  }
80
+ },
81
+ {
82
+ name: 'duck_duck_go',
83
+ label: 'DuckDuckGo Quick Answer',
84
+ description: 'Search for quick answers using DuckDuckGo API.',
85
+ beta: true,
86
+ parameters: {
87
+ type: 'object',
88
+ properties: {
89
+ query: { type: 'string', description: 'The search query' }
90
+ },
91
+ required: ['query']
92
+ }
93
+ },
94
+ {
95
+ name: 'duck_duck_go_scrape',
96
+ label: 'DuckDuckGo Scrape (Lite)',
97
+ description: 'Perform a full search on DuckDuckGo Lite and extract result links.',
98
+ beta: true,
99
+ parameters: {
100
+ type: 'object',
101
+ properties: {
102
+ query: { type: 'string', description: 'The search query' }
103
+ },
104
+ required: ['query']
105
+ }
106
+ },
107
+ {
108
+ name: 'patch_file',
109
+ label: 'Surgical File Patch (Beta)',
110
+ description: 'Edit a file by replacing specific sections of text. Much more efficient for large files.',
111
+ beta: true,
112
+ parameters: {
113
+ type: 'object',
114
+ properties: {
115
+ filepath: { type: 'string', description: 'Path to the file to patch' },
116
+ edits: {
117
+ type: 'array',
118
+ description: 'List of edits to perform in order',
119
+ items: {
120
+ type: 'object',
121
+ properties: {
122
+ oldText: { type: 'string', description: 'The exact text to find (including whitespace/indentation)' },
123
+ newText: { type: 'string', description: 'The text to replace it with' },
124
+ occurrence: {
125
+ type: 'integer',
126
+ description: 'Which occurrence of oldText to replace (1-based). Use 0 to replace all occurrences.',
127
+ default: 1
128
+ }
129
+ },
130
+ required: ['oldText', 'newText']
131
+ }
132
+ }
133
+ },
134
+ required: ['filepath', 'edits']
135
+ }
77
136
  }
78
137
  ];
79
138
 
139
+ export function getAvailableTools(config = {}) {
140
+ return TOOLS.filter(tool => {
141
+ if (tool.beta) {
142
+ return config.betaTools && config.betaTools.includes(tool.name);
143
+ }
144
+ return true;
145
+ });
146
+ }
147
+
80
148
  export async function executeTool(name, args) {
81
149
  switch (name) {
82
150
  case 'execute_command': return await execCommand(args);
@@ -85,6 +153,9 @@ export async function executeTool(name, args) {
85
153
  case 'fetch_url': return await fetchUrl(args);
86
154
  case 'search_files': return await searchFiles(args);
87
155
  case 'list_directory': return await listDirectory(args);
156
+ case 'duck_duck_go': return await duckDuckGo(args);
157
+ case 'duck_duck_go_scrape': return await duckDuckGoScrape(args);
158
+ case 'patch_file': return await patchFile(args);
88
159
  default: return `Unknown tool: ${name}`;
89
160
  }
90
161
  }
@@ -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
+ }