banana-code 1.3.1 → 1.4.1

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.
@@ -40,10 +40,15 @@ class ContextBuilder {
40
40
  constructor(fileManager, ignorePatterns = []) {
41
41
  this.fileManager = fileManager;
42
42
  this.projectDir = fileManager.projectDir;
43
- this.customIgnores = ignorePatterns;
43
+ this.customIgnores = [...ignorePatterns];
44
+ this.activeIgnores = [...this.customIgnores];
44
45
  this.loadedFiles = new Map(); // path -> content
45
46
  }
46
47
 
48
+ refreshIgnorePatterns() {
49
+ this.activeIgnores = [...new Set([...this.customIgnores, ...this.loadGitignore()])];
50
+ }
51
+
47
52
  /**
48
53
  * Load .gitignore patterns
49
54
  */
@@ -79,7 +84,7 @@ class ContextBuilder {
79
84
  if (IGNORED_FILES.includes(basename)) return true;
80
85
 
81
86
  // Check custom patterns (simple matching)
82
- for (const pattern of this.customIgnores) {
87
+ for (const pattern of this.activeIgnores) {
83
88
  if (pattern.endsWith('/') || pattern.endsWith('\\')) {
84
89
  // Directory pattern
85
90
  const dirName = pattern.slice(0, -1);
@@ -105,6 +110,9 @@ class ContextBuilder {
105
110
  * @param {object} counter - Shared counter across recursive calls
106
111
  */
107
112
  scanDirectory(dir = this.projectDir, depth = 0, maxDepth = 6, maxFiles = 5000, counter = { count: 0 }) {
113
+ if (depth === 0) {
114
+ this.refreshIgnorePatterns();
115
+ }
108
116
  if (depth > maxDepth) return [];
109
117
  if (counter.count >= maxFiles) return [];
110
118
 
@@ -262,8 +270,7 @@ class ContextBuilder {
262
270
  * Build the full context string for AI
263
271
  */
264
272
  buildContext() {
265
- // Load gitignore patterns
266
- this.customIgnores = [...this.customIgnores, ...this.loadGitignore()];
273
+ this.refreshIgnorePatterns();
267
274
 
268
275
  // Load priority files first
269
276
  this.loadPriorityFiles();
@@ -4,6 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
+ const { ensureDirSync, atomicWriteFileSync } = require('./fsUtils');
7
8
 
8
9
  // File extensions to always skip
9
10
  const BINARY_EXTENSIONS = [
@@ -29,9 +30,7 @@ class FileManager {
29
30
  * Ensure backup directory exists
30
31
  */
31
32
  ensureBackupDir() {
32
- if (!fs.existsSync(this.backupDir)) {
33
- fs.mkdirSync(this.backupDir, { recursive: true });
34
- }
33
+ ensureDirSync(this.backupDir);
35
34
  }
36
35
 
37
36
  /**
@@ -85,8 +84,9 @@ class FileManager {
85
84
  : path.join(this.projectDir, filePath);
86
85
 
87
86
  try {
87
+ const existed = fs.existsSync(fullPath);
88
88
  // Create backup if file exists
89
- if (fs.existsSync(fullPath)) {
89
+ if (existed) {
90
90
  this.ensureBackupDir();
91
91
  const timestamp = Date.now();
92
92
  const relativePath = path.relative(this.projectDir, fullPath);
@@ -97,15 +97,12 @@ class FileManager {
97
97
  }
98
98
 
99
99
  // Ensure directory exists
100
- const dir = path.dirname(fullPath);
101
- if (!fs.existsSync(dir)) {
102
- fs.mkdirSync(dir, { recursive: true });
103
- }
100
+ ensureDirSync(path.dirname(fullPath));
104
101
 
105
102
  // Write the file
106
- fs.writeFileSync(fullPath, content, 'utf-8');
103
+ atomicWriteFileSync(fullPath, content);
107
104
 
108
- return { success: true, path: fullPath, isNew: !fs.existsSync(fullPath) };
105
+ return { success: true, path: fullPath, isNew: !existed };
109
106
  } catch (error) {
110
107
  return { success: false, error: error.message };
111
108
  }
@@ -187,8 +184,9 @@ class FileManager {
187
184
  const targetPath = path.isAbsolute(filePath)
188
185
  ? filePath
189
186
  : path.join(this.projectDir, filePath);
187
+ ensureDirSync(path.dirname(targetPath));
190
188
 
191
- fs.writeFileSync(targetPath, content, 'utf-8');
189
+ atomicWriteFileSync(targetPath, content);
192
190
 
193
191
  return { success: true, restored: latestBackup.name };
194
192
  } catch (error) {
package/lib/fsUtils.js ADDED
@@ -0,0 +1,30 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+
5
+ function ensureDirSync(dirPath) {
6
+ if (!fs.existsSync(dirPath)) {
7
+ fs.mkdirSync(dirPath, { recursive: true });
8
+ }
9
+ }
10
+
11
+ function atomicWriteFileSync(targetPath, content, encoding = 'utf-8') {
12
+ const dir = path.dirname(targetPath);
13
+ ensureDirSync(dir);
14
+ const tempPath = path.join(
15
+ dir,
16
+ `.${path.basename(targetPath)}.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString('hex')}.tmp`
17
+ );
18
+ fs.writeFileSync(tempPath, content, encoding);
19
+ fs.renameSync(tempPath, targetPath);
20
+ }
21
+
22
+ function atomicWriteJsonSync(targetPath, value) {
23
+ atomicWriteFileSync(targetPath, JSON.stringify(value, null, 2));
24
+ }
25
+
26
+ module.exports = {
27
+ ensureDirSync,
28
+ atomicWriteFileSync,
29
+ atomicWriteJsonSync
30
+ };
@@ -4,6 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
+ const { ensureDirSync, atomicWriteFileSync } = require('./fsUtils');
7
8
 
8
9
  class HistoryManager {
9
10
  constructor(bananaDir, maxHistory = 100) {
@@ -44,11 +45,8 @@ class HistoryManager {
44
45
 
45
46
  save() {
46
47
  try {
47
- const dir = path.dirname(this.historyFile);
48
- if (!fs.existsSync(dir)) {
49
- fs.mkdirSync(dir, { recursive: true });
50
- }
51
- fs.writeFileSync(this.historyFile, this.history.join('\n'));
48
+ ensureDirSync(path.dirname(this.historyFile));
49
+ atomicWriteFileSync(this.historyFile, this.history.join('\n'));
52
50
  } catch {
53
51
  // Ignore save errors
54
52
  }
@@ -74,10 +74,10 @@ function pick(items, options = {}) {
74
74
 
75
75
  if (isSel) {
76
76
  const lead = showVisionIndicator ? `${pointer}${marker} ${vision} ` : `${pointer}${marker} `;
77
- return `${lead}${INVERSE} ${YELLOW}${key}${RESET}${INVERSE} ${label} ${RESET}${tags}`;
77
+ return `${lead}${INVERSE} ${YELLOW}${key}${RESET}${INVERSE} ${label} ${RESET}${desc}${tags}`;
78
78
  }
79
79
  const lead = showVisionIndicator ? `${pointer}${marker} ${vision} ` : `${pointer}${marker} `;
80
- return `${lead}${YELLOW}${key}${RESET} ${DIM}${label}${RESET}${tags}`;
80
+ return `${lead}${YELLOW}${key}${RESET} ${DIM}${label}${RESET}${desc}${tags}`;
81
81
  }
82
82
 
83
83
  // Total lines we render (title + items + footer)
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  const fs = require('fs');
7
+ const { atomicWriteJsonSync } = require('./fsUtils');
7
8
 
8
9
  class ModelRegistry {
9
10
  constructor(registryPath, lmStudio, providerStore = null) {
@@ -34,7 +35,7 @@ class ModelRegistry {
34
35
  }
35
36
 
36
37
  save() {
37
- fs.writeFileSync(this.registryPath, JSON.stringify(this.localRegistry, null, 2));
38
+ atomicWriteJsonSync(this.registryPath, this.localRegistry);
38
39
  }
39
40
 
40
41
  _normalizeLocalModels() {
@@ -68,7 +69,7 @@ class ModelRegistry {
68
69
  },
69
70
  tags: remoteModel.tags || ['remote', remoteModel.provider],
70
71
  tier: 'remote',
71
- description: `${remoteModel.name} via ${remoteModel.provider}`,
72
+ description: remoteModel.name,
72
73
  remote: true
73
74
  };
74
75
  }
@@ -18,7 +18,8 @@ const PROVIDER_LABELS = {
18
18
  anthropic: 'Anthropic',
19
19
  openai: 'OpenAI',
20
20
  openrouter: 'OpenRouter',
21
- monkey: 'Monkey Models'
21
+ monkey: 'Monkey Models',
22
+ 'claude-code': 'Claude Code'
22
23
  };
23
24
 
24
25
  class ProviderManager {
@@ -115,6 +116,11 @@ class ProviderManager {
115
116
  });
116
117
  }
117
118
 
119
+ if (provider === 'claude-code') {
120
+ const { ClaudeCodeClient } = require('./claudeCodeProvider');
121
+ return new ClaudeCodeClient();
122
+ }
123
+
118
124
  throw new Error(`Unknown provider: ${provider}`);
119
125
  }
120
126
 
@@ -4,7 +4,7 @@ const path = require('path');
4
4
 
5
5
  const DEFAULT_PATH = path.join(os.homedir(), '.banana', 'providers.json');
6
6
 
7
- const PROVIDERS = ['monkey', 'anthropic', 'openai', 'openrouter'];
7
+ const PROVIDERS = ['monkey', 'anthropic', 'openai', 'openrouter', 'claude-code'];
8
8
 
9
9
  const DEFAULT_PROVIDER_MODELS = {
10
10
  monkey: {},
@@ -42,6 +42,29 @@ const DEFAULT_PROVIDER_MODELS = {
42
42
  prompt: 'code-agent'
43
43
  }
44
44
  },
45
+ 'claude-code': {
46
+ 'opus': {
47
+ name: 'Claude Opus (Claude Code)',
48
+ id: 'opus',
49
+ contextLimit: 200000,
50
+ supportsThinking: false,
51
+ prompt: 'code-agent'
52
+ },
53
+ 'sonnet': {
54
+ name: 'Claude Sonnet (Claude Code)',
55
+ id: 'sonnet',
56
+ contextLimit: 200000,
57
+ supportsThinking: false,
58
+ prompt: 'code-agent'
59
+ },
60
+ 'haiku': {
61
+ name: 'Claude Haiku (Claude Code)',
62
+ id: 'haiku',
63
+ contextLimit: 200000,
64
+ supportsThinking: false,
65
+ prompt: 'code-agent'
66
+ }
67
+ },
45
68
  openrouter: {
46
69
  'claude-opus-4.6': {
47
70
  name: 'Claude Opus 4.6 (OpenRouter)',
@@ -91,7 +114,8 @@ function createDefaultData() {
91
114
  providers: {
92
115
  anthropic: defaultProviderRecord('anthropic'),
93
116
  openai: defaultProviderRecord('openai'),
94
- openrouter: defaultProviderRecord('openrouter')
117
+ openrouter: defaultProviderRecord('openrouter'),
118
+ 'claude-code': defaultProviderRecord('claude-code')
95
119
  }
96
120
  };
97
121
  }
@@ -328,6 +352,16 @@ class ProviderStore {
328
352
  this.save();
329
353
  }
330
354
 
355
+ /**
356
+ * Connect Claude Code provider (no API key needed, uses CLI auth).
357
+ */
358
+ connectClaudeCode() {
359
+ this._touch('claude-code');
360
+ this.data.providers['claude-code'].connected = true;
361
+ this.data.providers['claude-code'].auth = { type: 'cli' };
362
+ this.save();
363
+ }
364
+
331
365
  connectOpenAI(auth) {
332
366
  this._touch('openai');
333
367
  this.data.providers.openai.connected = true;
@@ -396,8 +430,8 @@ class ProviderStore {
396
430
  contextLimit: model.contextLimit || 128000,
397
431
  supportsThinking: model.supportsThinking !== false,
398
432
  prompt: model.prompt || 'code-agent',
399
- tags: provider === 'openrouter'
400
- ? ['remote', provider, 'tool-calling']
433
+ tags: (provider === 'openrouter' || provider === 'claude-code')
434
+ ? ['remote', provider]
401
435
  : ['remote', provider, 'tool-calling', 'vision']
402
436
  });
403
437
  }
@@ -36,6 +36,9 @@ class StreamHandler {
36
36
  // Buffer output until we confirm there's no orphan </think> coming
37
37
  this.orphanBuffer = '';
38
38
  this.orphanCheckComplete = false; // Once true, stop buffering for orphan check
39
+ this.completionStatus = 'completed';
40
+ this.completionWarning = null;
41
+ this.doneSignalReceived = false;
39
42
  }
40
43
 
41
44
  /**
@@ -176,7 +179,7 @@ class StreamHandler {
176
179
  async handleStream(response) {
177
180
  const reader = response.body.getReader();
178
181
  const decoder = new TextDecoder();
179
- const IDLE_TIMEOUT = 30000; // 30 seconds with no data = assume done (LM Studio needs time after prompt processing)
182
+ const IDLE_TIMEOUT = 30000;
180
183
 
181
184
  try {
182
185
  while (true) {
@@ -193,7 +196,8 @@ class StreamHandler {
193
196
  value = result.value;
194
197
  } catch (e) {
195
198
  if (e.message === 'IDLE_TIMEOUT') {
196
- // No data for 5 seconds, assume stream is complete
199
+ this.completionStatus = 'incomplete_stream';
200
+ this.completionWarning = `Stream stalled for ${IDLE_TIMEOUT}ms before an explicit completion signal.`;
197
201
  break;
198
202
  }
199
203
  throw e;
@@ -218,7 +222,9 @@ class StreamHandler {
218
222
 
219
223
  if (data === '[DONE]') {
220
224
  // Stream complete signal - exit the loop
221
- this.onComplete(this.fullResponse);
225
+ this.doneSignalReceived = true;
226
+ this.completionStatus = 'completed';
227
+ this.onComplete(this.fullResponse, this.getResult());
222
228
  return this.fullResponse;
223
229
  }
224
230
 
@@ -251,6 +257,11 @@ class StreamHandler {
251
257
  }
252
258
  }
253
259
 
260
+ if (!this.doneSignalReceived && this.completionStatus === 'completed') {
261
+ this.completionStatus = 'incomplete_stream';
262
+ this.completionWarning = 'Stream ended without an explicit completion signal.';
263
+ }
264
+
254
265
  // Process any remaining buffer
255
266
  if (this.buffer) {
256
267
  const lines = this.buffer.split('\n');
@@ -280,7 +291,7 @@ class StreamHandler {
280
291
  }
281
292
  }
282
293
 
283
- this.onComplete(this.fullResponse);
294
+ this.onComplete(this.fullResponse, this.getResult());
284
295
  return this.fullResponse;
285
296
 
286
297
  } catch (error) {
@@ -292,6 +303,16 @@ class StreamHandler {
292
303
  getFullResponse() {
293
304
  return this.fullResponse;
294
305
  }
306
+
307
+ getResult() {
308
+ return {
309
+ content: this.fullResponse,
310
+ completed: this.completionStatus === 'completed',
311
+ status: this.completionStatus,
312
+ warning: this.completionWarning,
313
+ doneSignalReceived: this.doneSignalReceived
314
+ };
315
+ }
295
316
  }
296
317
 
297
318
  // Non-streaming fallback with simulated streaming effect
package/package.json CHANGED
@@ -1,43 +1,48 @@
1
- {
2
- "name": "banana-code",
3
- "version": "1.3.1",
4
- "description": "AI coding agent CLI powered by Monkey Models and remote providers (Anthropic, OpenAI, OpenRouter)",
5
- "main": "banana.js",
6
- "bin": {
7
- "banana": "banana.js"
8
- },
9
- "files": [
10
- "banana.js",
11
- "lib/",
12
- "prompts/",
13
- "models.json",
14
- "package.json"
15
- ],
16
- "scripts": {
17
- "postinstall": "node scripts/createBananaDir.js || echo banana: skipped dir creation"
18
- },
19
- "keywords": [
20
- "ai",
21
- "coding",
22
- "agent",
23
- "cli",
24
- "monkey-models",
25
- "banana-code",
26
- "code-assistant"
27
- ],
28
- "author": "Matt Johnston",
29
- "license": "MIT",
30
- "repository": {
31
- "type": "git",
32
- "url": "git+https://github.com/mrchevyceleb/banana-code.git"
33
- },
34
- "homepage": "https://github.com/mrchevyceleb/banana-code",
35
- "engines": {
36
- "node": ">=18"
37
- },
38
- "dependencies": {
39
- "diff": "^8.0.3",
40
- "glob": "^13.0.6",
41
- "update-notifier": "^7.3.1"
42
- }
43
- }
1
+ {
2
+ "name": "banana-code",
3
+ "version": "1.4.1",
4
+ "description": "AI coding agent CLI powered by Monkey Models and remote providers (Anthropic, OpenAI, OpenRouter)",
5
+ "main": "banana.js",
6
+ "bin": {
7
+ "banana": "banana.js"
8
+ },
9
+ "files": [
10
+ "banana.js",
11
+ "lib/",
12
+ "prompts/",
13
+ "models.json",
14
+ "package.json"
15
+ ],
16
+ "scripts": {
17
+ "postinstall": "node scripts/createBananaDir.js || echo banana: skipped dir creation",
18
+ "test:tool-batch": "node scripts/regression-tool-batch-sanitizer.js",
19
+ "test:command-verification": "node scripts/regression-command-verification.js",
20
+ "test:stream-handler": "node scripts/regression-stream-handler.js",
21
+ "test:persistence-context": "node scripts/regression-persistence-context.js",
22
+ "test:reliability": "npm run test:tool-batch && npm run test:command-verification && npm run test:stream-handler && npm run test:persistence-context"
23
+ },
24
+ "keywords": [
25
+ "ai",
26
+ "coding",
27
+ "agent",
28
+ "cli",
29
+ "monkey-models",
30
+ "banana-code",
31
+ "code-assistant"
32
+ ],
33
+ "author": "Matt Johnston",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/mrchevyceleb/banana-code.git"
38
+ },
39
+ "homepage": "https://github.com/mrchevyceleb/banana-code",
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "dependencies": {
44
+ "diff": "^8.0.3",
45
+ "glob": "^13.0.6",
46
+ "update-notifier": "^7.3.1"
47
+ }
48
+ }
package/prompts/base.md CHANGED
@@ -1,23 +1,33 @@
1
- You are Banana, a sharp, *proactive* Executive assistant *AND* senior full-stack engineer. You are just as good at assisting people on multi-tool tasks and giving advice as you are a brilliant coder.
2
-
3
- ## Personality
4
- - You are always kind.
5
- - You are *tenacious*, you never give up
6
- - You are supportive, enthusiastic, and make people feel good about themselves.
7
- - Your knowledge of coding and software development is *second to none*
8
-
9
- ## General Knowledge
10
-
11
- You're a general-purpose assistant, not just a code tool. If the user asks about general knowledge topics, answer directly. Only use file/code tools when the question is actually about code or the project.
12
-
13
- ## OS
14
-
15
- Windows. Use PowerShell/cmd syntax for shell commands. No bash, no `ls`, no `grep`.
16
-
17
- ## File Operations
18
-
19
- Use the `create_file` and `edit_file` tools to write files. Use `run_command` for shell commands (PowerShell syntax only). Never dump file content as a markdown code block.
20
-
21
- ## Self-Awareness
22
-
23
- You ARE Banana Code. When users ask about your features, how to do something, or need guidance on setup, use the `banana_help` tool to look up accurate documentation. Topics: overview, hooks, models, commands, project-instructions, agents. Don't guess at feature details. Look them up, then guide the user through it naturally.
1
+ You are Banana, the AI inside Banana Code. You are a chill, supportive genius. Playful but relentless. You talk like a best friend who happens to be a world-class engineer. You are built for vibe coders who have big ideas but don't write code themselves.
2
+
3
+ ## Personality
4
+
5
+ - Always positive and encouraging. Match the user's energy.
6
+ - Never talk down to people. Explain things in a way that makes people feel smarter, not smaller.
7
+ - Brief, real, occasionally witty. No sycophancy. No "Certainly!" No walls of text.
8
+ - Tenacious. You never give up. You don't wait for approval unless genuinely required.
9
+ - When you make a mistake, own it with humor and fix it immediately.
10
+
11
+ ## General Knowledge
12
+
13
+ You're a general-purpose assistant, not just a code tool. If the user asks about general knowledge topics, answer directly. Only use file/code tools when the question is actually about code or the project.
14
+
15
+ ## OS
16
+
17
+ Windows. Use cmd.exe syntax for shell commands. No bash (grep, cat, ls). If you need PowerShell, wrap it: `powershell -Command "your command here"`.
18
+
19
+ ## File Operations
20
+
21
+ Use the `create_file` and `edit_file` tools to write files. Use `run_command` for shell commands. Never dump file content as a markdown code block.
22
+
23
+ ## Completeness
24
+
25
+ Never use placeholders. Every function is complete. Every component renders. If the feature needs 5 files, write all 5. If a vague prompt comes in, go all out and let the user steer from there.
26
+
27
+ ## Design Philosophy
28
+
29
+ Every frontend should feel like a toy: click easter eggs, floating animations, tactile hover effects, playful micro-interactions. Reject generic templates. Use distinctive typography, cohesive color palettes, orchestrated motion, and atmospheric backgrounds. Every project should look different.
30
+
31
+ ## Self-Awareness
32
+
33
+ You ARE Banana Code. When users ask about your features, how to do something, or need guidance on setup, use the `banana_help` tool to look up accurate documentation. Don't guess at feature details.
@@ -5,6 +5,7 @@
5
5
  - Always provide complete, valid JSON in tool call arguments. Never use partial or malformed JSON.
6
6
  - When calling tools, provide ALL required arguments. Missing arguments cause failures.
7
7
  - After receiving a tool result, analyze it before deciding your next step.
8
+ - If a command changes git state or the filesystem, verify the effect with a separate read-only check before you tell the user it is done.
8
9
  - Do not call the same tool with the same arguments twice in a row.
9
10
  - If a tool returns an error, try a different approach or explain the issue.
10
11
  - `run_command` uses cmd.exe on Windows, NOT PowerShell. Use cmd syntax (findstr, type, dir). For PowerShell, wrap it: `powershell -Command "..."`.