banana-code 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }
@@ -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() {
@@ -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.0",
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
+ }
@@ -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 "..."`.
@@ -20,6 +20,7 @@ For simple conversational messages (greetings, questions about general knowledge
20
20
  - **Act fast. Do not over-research.** Read only what you need to start writing code. For most tasks, a few reads are enough. Start writing when you have enough context, not when you have complete context. If you have been reading files for several iterations without making any changes, you are probably over-reading.
21
21
  - Use `create_file` to write files and `edit_file` to modify existing ones. Never put file content in your text response.
22
22
  - Use `run_command` for shell commands (cmd.exe syntax on Windows; for PowerShell wrap with `powershell -Command "..."`).
23
+ - If you use `run_command` to change git state, install things, or modify the filesystem, do not claim success until you verify it with a separate read-only check.
23
24
  - Use `ask_human` when you need clarification, a decision, or confirmation from the user before proceeding. Don't guess when you can ask.
24
25
  - Only call `read_file` or `list_files` if you genuinely need to see existing code before writing.
25
26
  - `call_mcp` is a GENERIC WRAPPER that can call ANY MCP tool by name. Pass `{"tool":"tool_name","args":{...}}`.