@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.
- package/LICENSE +674 -0
- package/README.md +5 -1
- package/package.json +6 -3
- package/src/config.js +26 -4
- package/src/constants.js +11 -0
- package/src/index.js +197 -14
- package/src/prompt.js +25 -2
- package/src/providers/claude.js +20 -7
- package/src/providers/gemini.js +28 -7
- package/src/providers/ollama.js +28 -6
- package/src/providers/ollamaCloud.js +107 -0
- package/src/providers/openai.js +54 -22
- package/src/tools/duckDuckGo.js +34 -0
- package/src/tools/duckDuckGoScrape.js +93 -0
- package/src/tools/execCommand.js +6 -2
- package/src/tools/patchFile.js +63 -0
- package/src/tools/registry.js +71 -0
- package/src/utils/markdown.js +21 -0
- package/src/utils/workspace.js +30 -0
- package/test-codex.js +0 -5
package/src/providers/ollama.js
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
|
-
import {
|
|
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.
|
|
12
|
-
this.
|
|
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
|
-
|
|
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
|
+
}
|
package/src/providers/openai.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
-
import {
|
|
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.
|
|
20
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
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
|
}
|
|
@@ -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
|
+
}
|
package/src/tools/registry.js
CHANGED
|
@@ -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
|
+
}
|