@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.
@@ -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
- process.stdout.write(chalk.cyan(event.delta.text));
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
 
@@ -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
- process.stdout.write(chalk.cyan(part.text));
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
@@ -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
- 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
+ }
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
+ }
@@ -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
- 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
+ }
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
- 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
+ }
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
+ }
@@ -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
  }
@@ -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
+ }