banana-code 1.2.0 → 1.3.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.
package/banana.js CHANGED
@@ -21,6 +21,7 @@ const readline = require('readline');
21
21
  const path = require('path');
22
22
  const fs = require('fs');
23
23
  const os = require('os');
24
+ const updateNotifier = require('update-notifier');
24
25
 
25
26
  const FileManager = require('./lib/fileManager');
26
27
  const ContextBuilder = require('./lib/contextBuilder');
@@ -80,7 +81,7 @@ let pendingHumanQuestion = null; // { resolve, question }
80
81
  // CONFIGURATION
81
82
  // =============================================================================
82
83
 
83
- const VERSION = '1.2.0';
84
+ const VERSION = '1.3.1';
84
85
  const { PAD } = require('./lib/borderRenderer'); // Single source of truth for left padding
85
86
  const DEBUG_DISABLED_VALUES = new Set(['0', 'false', 'off', 'no']);
86
87
  const NEXT_TURN_RESERVE_TOKENS = 1200;
@@ -257,6 +258,19 @@ function logSessionEnd(reason, detail = '') {
257
258
  appendDebugLog(`=== session_end ${new Date().toISOString()} reason=${reason}${detail} ===\n`);
258
259
  }
259
260
 
261
+ function notifyIfUpdateAvailable() {
262
+ if (!process.stdout.isTTY) return;
263
+ try {
264
+ const notifier = updateNotifier({
265
+ pkg: { name: 'banana-code', version: VERSION },
266
+ updateCheckInterval: 1000 * 60 * 60 * 12
267
+ });
268
+ notifier.notify({ isGlobal: true, defer: false });
269
+ } catch {
270
+ // Update checks are best-effort; never fail startup.
271
+ }
272
+ }
273
+
260
274
  // =============================================================================
261
275
  // ASCII ART & UI
262
276
  // =============================================================================
@@ -391,6 +405,8 @@ ${P}${c.yellow}/run <cmd>${c.reset} Run a shell command
391
405
  ${P}${c.yellow}/undo${c.reset} Show recent backups
392
406
  ${P}${c.yellow}/restore <path>${c.reset} Restore file from backup
393
407
  ${P}${c.yellow}/commands${c.reset} List custom commands (~/.banana/commands/)
408
+ ${P}${c.yellow}/update${c.reset} Update Banana via npm
409
+
394
410
  ${P}${c.yellow}/version${c.reset} Show version
395
411
  ${P}${c.yellow}/help${c.reset} Show this help
396
412
  ${P}${c.yellow}/exit${c.reset} Exit Banana
@@ -608,7 +624,8 @@ async function getActiveClient() {
608
624
  }
609
625
 
610
626
  async function syncLocalModelToLmStudio(activeModel) {
611
- if (!activeModel || (activeModel.provider || 'local') !== 'local' || !activeModel.id) return;
627
+ if (!activeModel || activeModel.dynamic) return; // Dynamic LM Studio models are user-managed
628
+ if ((activeModel.provider || 'local') !== 'local' || !activeModel.id) return;
612
629
  try {
613
630
  const loaded = await lmStudio.getLoadedInstances();
614
631
  const activeLoaded = loaded.some(inst => (inst.key || inst.id || '').includes(activeModel.id));
@@ -638,14 +655,13 @@ async function checkConnection() {
638
655
  const activeModel = modelRegistry.getCurrentModel();
639
656
 
640
657
  const lmConnected = await lmStudio.isConnected();
658
+ await modelRegistry.refreshLmStudio();
641
659
  if (lmConnected) {
642
- console.log(`${PAD}${c.green}✓${c.reset} LM Studio: Connected (${lmStudio.baseUrl})`);
643
- const discovery = await modelRegistry.discover();
644
- if (discovery.matched > 0) {
645
- console.log(`${PAD}${c.green}✓${c.reset} Models: ${discovery.matched} matched from ${discovery.total} loaded`);
646
- }
647
- if (activeProvider === 'local') {
648
- await syncLocalModelToLmStudio(activeModel);
660
+ const lmModel = modelRegistry.get('lmstudio');
661
+ if (lmModel && lmModel.id) {
662
+ console.log(`${PAD}${c.green}✓${c.reset} LM Studio: ${lmModel.name} (${lmStudio.baseUrl})`);
663
+ } else {
664
+ console.log(`${PAD}${c.yellow}○${c.reset} LM Studio: Connected, no model loaded (${lmStudio.baseUrl})`);
649
665
  }
650
666
  } else {
651
667
  const prefix = activeProvider === 'local' ? c.red : c.yellow;
@@ -1234,29 +1250,14 @@ async function switchModel(modelKey, options = {}) {
1234
1250
 
1235
1251
  const provider = switched.provider || 'local';
1236
1252
  if (provider === 'local') {
1237
- if (switched.id && (!oldModel || switched.id !== oldModel.id || (oldModel.provider || 'local') !== 'local')) {
1238
- try {
1239
- const loaded = await lmStudio.getLoadedInstances();
1240
- for (const inst of loaded) {
1241
- try {
1242
- await lmStudio.unloadModel(inst.instanceId);
1243
- if (!silent) console.log(`${PAD}${c.dim}Unloaded ${inst.displayName || inst.key}${c.reset}`);
1244
- } catch (err) {
1245
- // 404 = model already ejected by LM Studio (normal during swap)
1246
- if (!silent && !err.message.includes('404')) {
1247
- console.log(`${PAD}${c.yellow}⚠ Could not unload ${inst.key}: ${err.message}${c.reset}`);
1248
- }
1249
- }
1250
- }
1251
- const ctxLen = switched.contextLimit || 32768;
1252
- if (!silent) console.log(`${PAD}${c.dim}Loading ${switched.name} in LM Studio (ctx: ${(ctxLen / 1024).toFixed(0)}K)...${c.reset}`);
1253
- const result = await lmStudio.loadModel(switched.id, { contextLength: ctxLen });
1254
- if (!silent) console.log(`${PAD}${c.green}✓ Loaded in ${result.load_time_seconds?.toFixed(1)}s${c.reset}`);
1255
- } catch (err) {
1256
- if (!silent) {
1257
- console.log(`${PAD}${c.yellow}⚠ Could not auto-load: ${err.message}${c.reset}`);
1258
- console.log(`${PAD}${c.dim}Load it manually in LM Studio${c.reset}`);
1259
- }
1253
+ if (switched.dynamic) {
1254
+ // Dynamic LM Studio model - refresh to get latest loaded model info
1255
+ await modelRegistry.refreshLmStudio();
1256
+ const refreshed = modelRegistry.get('lmstudio');
1257
+ if (refreshed && refreshed.id) {
1258
+ if (!silent) console.log(`${PAD}${c.green}✓ Using ${refreshed.name}${c.reset}`);
1259
+ } else {
1260
+ if (!silent) console.log(`${PAD}${c.yellow}⚠ LM Studio connected but no model loaded${c.reset}`);
1260
1261
  }
1261
1262
  }
1262
1263
  if (switched.tags?.includes('vision') && !silent) {
@@ -1372,7 +1373,7 @@ async function handleCommand(input) {
1372
1373
  console.log(`\n${PAD}${c.cyan}Checking for updates...${c.reset}`);
1373
1374
  const { execSync } = require('child_process');
1374
1375
  try {
1375
- const result = execSync('npm install -g mrchevyceleb/banana-code', {
1376
+ const result = execSync('npm install -g banana-code@latest', {
1376
1377
  encoding: 'utf-8',
1377
1378
  timeout: 60000,
1378
1379
  stdio: ['pipe', 'pipe', 'pipe']
@@ -2004,7 +2005,8 @@ async function handleCommand(input) {
2004
2005
  const modelArg = modelArgRaw.toLowerCase();
2005
2006
 
2006
2007
  if (!modelArg) {
2007
- // Interactive model picker
2008
+ // Refresh LM Studio state before showing picker
2009
+ await modelRegistry.refreshLmStudio();
2008
2010
  const models = modelRegistry.list();
2009
2011
  const current = modelRegistry.getCurrent();
2010
2012
  const pickerItems = models.map(m => ({
@@ -2793,12 +2795,19 @@ async function sendStreamingMessage(message, images = [], rawMessage = '') {
2793
2795
  if (_renderingWorkPrompt) return; // prevent re-entrancy cascade
2794
2796
  _renderingWorkPrompt = true;
2795
2797
  try {
2796
- // rl.prompt(false) redraws the prompt + whatever is already in rl.line.
2797
- // Do NOT call rl.write(savedLine) here - that would re-inject the text
2798
- // through readline's input path, doubling it in the buffer and on screen.
2798
+ // rl.prompt(false) redraws the prompt + whatever is already in rl.line,
2799
+ // but always places cursor at end. Save/restore cursor position so the
2800
+ // user can keep typing mid-line without the cursor jumping.
2801
+ const savedCursor = rl.cursor;
2799
2802
  process.stdout.write(`${fitToTerminal(getStreamSpinnerText())}\n\n`);
2800
2803
  rl.setPrompt(buildPromptPrefix());
2801
2804
  rl.prompt(false);
2805
+ if (savedCursor < rl.line.length) {
2806
+ // Move cursor back from end-of-line to saved position
2807
+ const moveBack = rl.line.length - savedCursor;
2808
+ process.stdout.write(`\x1b[${moveBack}D`);
2809
+ rl.cursor = savedCursor;
2810
+ }
2802
2811
  if (statusBar) statusBar.render();
2803
2812
  } finally {
2804
2813
  _renderingWorkPrompt = false;
@@ -2900,6 +2909,11 @@ async function sendStreamingMessage(message, images = [], rawMessage = '') {
2900
2909
  startThinking();
2901
2910
  if (statusBar) statusBar.startTiming();
2902
2911
 
2912
+ // Refresh LM Studio model info if active (picks up model swaps between messages)
2913
+ if (modelRegistry.getCurrentModel()?.dynamic) {
2914
+ await modelRegistry.refreshLmStudio();
2915
+ }
2916
+
2903
2917
  const streamInferenceSettings = modelRegistry.getInferenceSettings();
2904
2918
  const activeClient = await getActiveClient();
2905
2919
  const streamModelMeta = modelRegistry.getCurrentModel();
@@ -3557,12 +3571,18 @@ async function sendAgenticMessage(message, images = [], rawMessage = '') {
3557
3571
  if (_renderingWorkPrompt) return; // prevent re-entrancy cascade
3558
3572
  _renderingWorkPrompt = true;
3559
3573
  try {
3560
- // rl.prompt(false) redraws the prompt + whatever is already in rl.line.
3561
- // Do NOT call rl.write(savedLine) here - that would re-inject the text
3562
- // through readline's input path, doubling it in the buffer and on screen.
3574
+ // rl.prompt(false) redraws the prompt + whatever is already in rl.line,
3575
+ // but always places cursor at end. Save/restore cursor position so the
3576
+ // user can keep typing mid-line without the cursor jumping.
3577
+ const savedCursor = rl.cursor;
3563
3578
  process.stdout.write(`${fitToTerminal(getSpinnerText())}\n\n`);
3564
3579
  rl.setPrompt(buildPromptPrefix());
3565
3580
  rl.prompt(false);
3581
+ if (savedCursor < rl.line.length) {
3582
+ const moveBack = rl.line.length - savedCursor;
3583
+ process.stdout.write(`\x1b[${moveBack}D`);
3584
+ rl.cursor = savedCursor;
3585
+ }
3566
3586
  if (statusBar) statusBar.render();
3567
3587
  } finally {
3568
3588
  _renderingWorkPrompt = false;
@@ -3923,11 +3943,16 @@ async function sendAgenticMessage(message, images = [], rawMessage = '') {
3923
3943
  if (_renderingWorkPrompt) return; // prevent re-entrancy cascade
3924
3944
  _renderingWorkPrompt = true;
3925
3945
  try {
3926
- // rl.prompt(false) redraws the prompt + whatever is already in rl.line.
3927
- // Do NOT call rl.write(savedLine) here - that would re-inject the text
3928
- // through readline's input path, doubling it in the buffer and on screen.
3946
+ // rl.prompt(false) redraws the prompt + whatever is already in rl.line,
3947
+ // but always places cursor at end. Save/restore cursor position.
3948
+ const savedCursor = rl.cursor;
3929
3949
  process.stdout.write(`${fitToTerminal(getSpinnerText())}\n`);
3930
3950
  rl.prompt(false);
3951
+ if (savedCursor < rl.line.length) {
3952
+ const moveBack = rl.line.length - savedCursor;
3953
+ process.stdout.write(`\x1b[${moveBack}D`);
3954
+ rl.cursor = savedCursor;
3955
+ }
3931
3956
  if (statusBar) statusBar.render();
3932
3957
  } finally {
3933
3958
  _renderingWorkPrompt = false;
@@ -4005,6 +4030,11 @@ async function sendAgenticMessage(message, images = [], rawMessage = '') {
4005
4030
  });
4006
4031
  currentRunner = runner;
4007
4032
 
4033
+ // Refresh LM Studio model info if active (picks up model swaps between messages)
4034
+ if (modelRegistry.getCurrentModel()?.dynamic) {
4035
+ await modelRegistry.refreshLmStudio();
4036
+ }
4037
+
4008
4038
  // Use model-specific inference settings
4009
4039
  currentAbortController = new AbortController();
4010
4040
  const inferenceSettings = modelRegistry.getInferenceSettings();
@@ -4949,6 +4979,7 @@ Examples:
4949
4979
  // Show banner and initialize
4950
4980
  await showBanner();
4951
4981
  initProject();
4982
+ notifyIfUpdateAvailable();
4952
4983
  if (runtimeLogPath) {
4953
4984
  console.log(`${PAD}${c.dim}Logs: ${runtimeLogPath}${c.reset}`);
4954
4985
  }
@@ -548,7 +548,7 @@ const READ_ONLY_TOOL_NAMES = new Set(['read_file', 'list_files', 'search_code',
548
548
  const READ_ONLY_TOOLS = TOOLS.filter(t => READ_ONLY_TOOL_NAMES.has(t.function.name));
549
549
 
550
550
  const IGNORE_PATTERNS = ['node_modules', '.git', '.next', 'dist', 'build', '.banana'];
551
- const MAX_ITERATIONS = 30;
551
+ const MAX_ITERATIONS = 50;
552
552
  const WRITE_TOOL_NAMES = new Set(['create_file', 'edit_file', 'run_command']);
553
553
  const CONTEXT_TRIM_THRESHOLD = 0.60; // 60% of context limit - start trimming early
554
554
  const CONTEXT_TRIM_KEEP_RECENT = 6; // Keep last N messages intact
@@ -1304,6 +1304,16 @@ class AgenticRunner {
1304
1304
  const failedMcpTools = new Set(); // Track MCP tools that returned "Unknown tool" errors
1305
1305
  let readOnlyStreak = 0; // Consecutive iterations with only read-only tool calls
1306
1306
  let loopWarningCount = 0; // How many times loop detection has fired
1307
+
1308
+ // Model-tier-aware read-only thresholds: smarter models get more research leeway
1309
+ // options.model is the raw model ID (e.g. "claude-sonnet-4-6-20250514", "gpt-4o", "silverback")
1310
+ const modelId = (options.model || '').toLowerCase();
1311
+ const isFrontier = /claude|gpt-4|gpt-5|o[1-9]-|o[1-9]$|chatgpt/.test(modelId) || modelId.includes('silverback');
1312
+ const readOnlyThresholds = isFrontier
1313
+ ? { nudge: 15, escalate: 20, hardStop: 25 }
1314
+ : (modelId.includes('tamarin'))
1315
+ ? { nudge: 6, escalate: 10, hardStop: 14 }
1316
+ : /* mandrill, local, openrouter, or unknown */ { nudge: 10, escalate: 15, hardStop: 20 };
1307
1317
  const writtenFiles = [];
1308
1318
  this._lastWrittenFiles = null;
1309
1319
  this._pendingSteers = [];
@@ -1473,6 +1483,14 @@ class AgenticRunner {
1473
1483
  // Some models use finish_reason "tool_calls", others use "stop" or "function_call"
1474
1484
  // but still include tool_calls in the message. Check for the array itself.
1475
1485
  if (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) {
1486
+ // Cap tool calls per response to prevent runaway models spamming dozens of calls
1487
+ const MAX_TOOL_CALLS_PER_RESPONSE = 8;
1488
+ if (assistantMessage.tool_calls.length > MAX_TOOL_CALLS_PER_RESPONSE) {
1489
+ appendDebugLog(` [TOOL CALL CAP] Model returned ${assistantMessage.tool_calls.length} tool calls, capping to ${MAX_TOOL_CALLS_PER_RESPONSE}\n`);
1490
+ this.onWarning(`Model tried to make ${assistantMessage.tool_calls.length} tool calls at once. Capping to ${MAX_TOOL_CALLS_PER_RESPONSE}.`);
1491
+ assistantMessage.tool_calls = assistantMessage.tool_calls.slice(0, MAX_TOOL_CALLS_PER_RESPONSE);
1492
+ }
1493
+
1476
1494
  // Add assistant message to history, preserving the reasoning field
1477
1495
  // GPT-OSS requires the reasoning/CoT from prior tool calls to be passed back.
1478
1496
  // Other models ignore it harmlessly.
@@ -1663,24 +1681,36 @@ class AgenticRunner {
1663
1681
  }
1664
1682
 
1665
1683
  // Track read-only streaks (iterations with no writes or commands)
1684
+ // Skip streak tracking in plan mode - plan mode is inherently read-only
1666
1685
  const thisIterToolNames = assistantMessage.tool_calls.map(t => t.function.name);
1667
- const hadWriteAction = thisIterToolNames.some(n => WRITE_TOOL_NAMES.has(n));
1668
- if (hadWriteAction) {
1669
- readOnlyStreak = 0;
1670
- } else {
1671
- readOnlyStreak++;
1686
+ if (!options.readOnly) {
1687
+ const hadWriteAction = thisIterToolNames.some(n => WRITE_TOOL_NAMES.has(n));
1688
+ if (hadWriteAction) {
1689
+ readOnlyStreak = 0;
1690
+ } else {
1691
+ readOnlyStreak++;
1692
+ }
1672
1693
  }
1673
1694
 
1674
- // Read-only streak nudge: after 5 consecutive read-only iterations, nudge to act
1675
- if (readOnlyStreak === 5) {
1676
- appendDebugLog(` [READ-ONLY NUDGE] ${readOnlyStreak} consecutive read-only iterations\n`);
1677
- this.onWarning('5 consecutive read-only iterations. Nudging to start implementing.');
1695
+ // Read-only streak nudges: escalating pressure, thresholds vary by model tier
1696
+ if (readOnlyStreak === readOnlyThresholds.nudge) {
1697
+ appendDebugLog(` [READ-ONLY NUDGE] ${readOnlyStreak} consecutive read-only iterations (threshold: ${readOnlyThresholds.nudge})\n`);
1698
+ this.onWarning(`${readOnlyStreak} consecutive read-only iterations. Nudging to start implementing.`);
1678
1699
  messages.push({
1679
1700
  role: 'system',
1680
- content: 'NOTICE: You have done 5 consecutive read/search iterations without writing any code or running commands. ' +
1701
+ content: `NOTICE: You have done ${readOnlyStreak} consecutive read/search iterations without writing any code or running commands. ` +
1681
1702
  'You likely have enough context now. Start implementing changes. ' +
1682
1703
  'If you are genuinely blocked, use ask_human to tell the user what you need.'
1683
1704
  });
1705
+ } else if (readOnlyStreak === readOnlyThresholds.escalate) {
1706
+ appendDebugLog(` [READ-ONLY ESCALATION] ${readOnlyStreak} consecutive read-only iterations (threshold: ${readOnlyThresholds.escalate})\n`);
1707
+ this.onWarning(`${readOnlyStreak} consecutive read-only iterations. You MUST start writing code now.`);
1708
+ messages.push({
1709
+ role: 'system',
1710
+ content: `URGENT: You have done ${readOnlyStreak} consecutive read/search iterations. You are over-researching. ` +
1711
+ 'STOP reading files. STOP searching. Write code NOW using create_file or edit_file. ' +
1712
+ 'If you cannot proceed, use ask_human immediately. Do NOT call read_file or search_code again.'
1713
+ });
1684
1714
  }
1685
1715
 
1686
1716
  // Loop detection: inject nudge or hard-break after all tool results
@@ -1738,8 +1768,8 @@ class AgenticRunner {
1738
1768
  }
1739
1769
  }
1740
1770
 
1741
- // Hard break if 8+ consecutive read-only iterations with no progress
1742
- if (readOnlyStreak >= 8) {
1771
+ // Hard break based on model tier threshold
1772
+ if (readOnlyStreak >= readOnlyThresholds.hardStop) {
1743
1773
  appendDebugLog(` [NO-PROGRESS BREAKER] ${readOnlyStreak} consecutive read-only iterations\n`);
1744
1774
  this.onWarning(`No write actions for ${readOnlyStreak} consecutive iterations. Stopping.`);
1745
1775
  messages.push({
@@ -1,254 +1,258 @@
1
- /**
2
- * Interactive arrow-key picker for Banana Code
3
- *
4
- * Renders a navigable list in the terminal. Arrow keys move highlight,
5
- * Enter selects, Escape cancels. Temporarily takes over stdin.
6
- */
7
-
8
- const readline = require('readline');
9
-
10
- const ESC = '\x1b';
11
- const HIDE_CURSOR = `${ESC}[?25l`;
12
- const SHOW_CURSOR = `${ESC}[?25h`;
13
- const CLEAR_LINE = `${ESC}[2K`;
14
- const MOVE_UP = (n) => n > 0 ? `${ESC}[${n}A` : '';
15
- const MOVE_COL0 = `${ESC}[0G`;
16
-
17
- // 256-color codes matching diffViewer.js palette
18
- const DIM = `${ESC}[2m`;
19
- const RESET = `${ESC}[0m`;
20
- const GREEN = `${ESC}[38;5;120m`;
21
- const YELLOW = `${ESC}[38;5;226m`;
22
- const CYAN = `${ESC}[38;5;51m`;
23
- const GRAY = `${ESC}[38;5;245m`;
24
- const INVERSE = `${ESC}[7m`;
25
-
26
- // Flag indicating a picker UI is active. When true, the global keypress
27
- // handler in banana.js should not process keys (arrow keys, escape, etc.)
28
- // because the picker owns stdin during its lifetime.
29
- let _pickerActive = false;
30
- function isPickerActive() { return _pickerActive; }
31
-
32
- /**
33
- * Show an interactive picker menu.
34
- *
35
- * @param {Array} items - Array of { key, label, description, active, tags }\r\n * @param {Object} options\r\n * @param {string} options.title - Header text\r\n * @param {number} options.selected - Initial selected index (default: active item or 0)\r\n * @returns {Promise<<ObjectObject|null>} - Selected item, or null if cancelled\r\n */
36
- function pick(items, options = {}) {
37
- return new Promise((resolve) => {
38
- if (!items || items.length === 0) {
39
- resolve(null);
40
- return;
41
- }
42
-
43
- let selected = options.selected ?? items.findIndex(i => i.active);
44
- if (selected << 0) selected = 0;
45
-
46
- const out = process.stdout;
47
- const title = options.title || 'Select:';
48
- const showVisionIndicator = options.showVisionIndicator === true;
49
- const footerText = showVisionIndicator
50
- ? `${DIM} ↑↓ navigate Enter select Esc cancel V=vision${RESET}`
51
- : `${DIM} ↑↓ navigate Enter select Esc cancel${RESET}`;
52
-
53
- // Pad key names to align descriptions
54
- const maxKeyLen = Math.max(...items.map(i => (i.key || '').length));
55
-
56
- function formatItem(item, index) {
57
- const isSel = index === selected;
58
- const isActive = item.active;
59
- const marker = isActive ? `${GREEN}●${RESET}` : `${GRAY}○${RESET}`;
60
- const pointer = isSel ? `${CYAN}▸${RESET} ` : ' ';
61
- const hasVision = Array.isArray(item.tags) && item.tags.includes('vision');
62
- const vision = showVisionIndicator
63
- ? (hasVision ? `${CYAN}V${RESET}` : `${DIM}.${RESET}`)
64
- : '';
65
- const key = (item.key || '').padEnd(maxKeyLen);
66
- const tags = item.tags?.length ? ` ${DIM}[${item.tags.join(', ')}]${RESET}` : '';
67
- const desc = item.description ? ` ${DIM}- ${item.description}${RESET}` : '';
68
- const label = item.label || item.name || item.key;
69
-
70
- if (isSel) {
71
- const lead = showVisionIndicator ? `${pointer}${marker} ${vision} ` : `${pointer}${marker} `;
72
- return `${lead}${INVERSE} ${YELLOW}${key}${RESET}${INVERSE} ${label} ${RESET}${tags}`;
73
- }
74
- const lead = showVisionIndicator ? `${pointer}${marker} ${vision} ` : `${pointer}${marker} `;
75
- return `${lead}${YELLOW}${key}${RESET} ${DIM}${label}${RESET}${tags}`;
76
- }
77
-
78
- // Total lines we render (title + items + footer)
79
- const totalLines = 1 + items.length + 1;
80
-
81
- function render() {
82
- _pickerActive = true;
83
- let buf = HIDE_CURSOR;
84
- buf += `\n${CYAN} ${title}${RESET}\n`;
85
- for (let i = 0; i << items items.length; i++) {
86
- buf += ` ${formatItem(items[i], i)}\n`;
87
- }
88
- buf += `${footerText}\n`;
89
- out.write(buf);
90
- }
91
-
92
- function redraw() {
93
- // Move up to the start of the picker area
94
- let buf = MOVE_UP(totalLines) + MOVE_COL0;
95
- // Clear all lines in the area
96
- for (let i = 0; i << total totalLines; i++) {
97
- buf += CLEAR_LINE + '\n';
98
- }
99
- // Move back up to the start to re-render
100
- buf += MOVE_UP(totalLines) + MOVE_COL0;
101
- buf += `${CYAN} ${title}${RESET}\n`;
102
- for (let i = 0; i << items items.length; i++) {
103
- buf += ` ${formatItem(items[i], i)}\n`;
104
- }
105
- buf += `${footerText}\n`;
106
- out.write(buf);
107
- }
108
-
109
- function cleanup() {
110
- _pickerActive = false;
111
- process.stdin.removeListener('keypress', onKey);
112
- out.write(SHOW_CURSOR);
113
- }
114
-
115
- function onKey(str, key) {
116
- if (!key) return;
117
- try {
118
- if (key.name === 'up') {
119
- selected = (selected - 1 + items.length) % items.length;
120
- redraw();
121
- } else if (key.name === 'down') {
122
- selected = (selected + 1) % items.length;
123
- redraw();
124
- } else if (key.name === 'return') {
125
- cleanup();
126
- resolve(items[selected]);
127
- } else if (key.name === 'escape') {
128
- cleanup();
129
- resolve(null);
130
- }
131
- } catch (err) {
132
- cleanup();
133
- resolve(null);
134
- }
135
- }
136
-
137
- // Ensure keypress events are flowing
138
- if (!process.stdin.listenerCount('keypress')) {
139
- readline.emitKeypressEvents(process.stdin);
140
- }
141
-
142
- render();
143
- process.stdin.on('keypress', onKey);
144
- });
145
- }
146
-
147
- /**
148
- * Interactive toggle list. Items can be toggled on/off with Space.
149
- * Arrow keys navigate, Space toggles, Escape exits.
150
- *
151
- * @param {Array} items - Array of { key, label, description, enabled, meta }\r\n * @param {Object} options\r\n * @param {string} options.title - Header text\r\n * @param {Function} options.onToggle - Called with (item, newEnabled) when toggled. Should return true if toggle succeeded.\r\n * @returns {Promise<<voidvoid>} - Resolves when user presses Escape\r\n */
152
- function pickToggle(items, options = {}) {
153
- return new Promise((resolve) => {
154
- if (!items || items.length === 0) {
155
- resolve();
156
- return;
157
- }
158
-
159
- let selected = 0;
160
- const out = process.stdout;
161
- const title = options.title || 'Toggle:';
162
- const onToggle = options.onToggle || (() => true);
163
- const RED = `${ESC}[38;5;203m`;
164
- const footerText = `${DIM} ↑↓ navigate Space toggle Enter/Esc done${RESET}`;
165
-
166
- const maxKeyLen = Math.max(...items.map(i => (i.key || '').length));
167
-
168
- function formatItem(item, index) {
169
- const isSel = index === selected;
170
- const pointer = isSel ? `${CYAN}▸${RESET} ` : ' ';
171
- const toggle = item.enabled ? `${GREEN}on ${RESET}` : `${RED}off${RESET}`;
172
- const key = (item.key || '').padEnd(maxKeyLen);
173
- const desc = item.description ? ` ${DIM}${item.description}${RESET}` : '';
174
- const meta = item.meta ? ` ${DIM}[${item.meta}]${RESET}` : '';
175
-
176
- if (isSel) {
177
- return `${pointer}[${toggle}] ${INVERSE} ${YELLOW}${key}${RESET}${INVERSE} ${RESET}${desc}${meta}`;
178
- }
179
- return `${pointer}[${toggle}] ${YELLOW}${key}${RESET}${desc}${meta}`;
180
- }
181
-
182
- const totalLines = 1 + items.length + 1;
183
-
184
- function render() {
185
- _pickerActive = true;
186
- let buf = HIDE_CURSOR;
187
- buf += `\n${CYAN} ${title}${RESET}\n`;
188
- for (let i = 0; i << items items.length; i++) {
189
- buf += ` ${formatItem(items[i], i)}\n`;
190
- }
191
- buf += `${footerText}\n`;
192
- out.write(buf);
193
- }
194
-
195
- function redraw() {
196
- // Move up to the start of the toggle area
197
- let buf = MOVE_UP(totalLines) + MOVE_COL0;
198
- // Clear all lines in the area
199
- for (let i = 0; i << total totalLines; i++) {
200
- buf += CLEAR_LINE + '\n';
201
- }
202
- // Move back up to the start to re-render
203
- buf += MOVE_UP(totalLines) + MOVE_COL0;
204
- buf += `${CYAN} ${title}${RESET}\n`;
205
- for (let i = 0; i << items items.length; i++) {
206
- buf += ` ${formatItem(items[i], i)}\n`;
207
- }
208
- buf += `${footerText}\n`;
209
- out.write(buf);
210
- }
211
-
212
- function cleanup() {
213
- _pickerActive = false;
214
- process.stdin.removeListener('keypress', onKey);
215
- out.write(SHOW_CURSOR);
216
- }
217
-
218
- function onKey(str, key) {
219
- if (!key) return;
220
- try {
221
- if (key.name === 'up') {
222
- selected = (selected - 1 + items.length) % items.length;
223
- redraw();
224
- } else if (key.name === 'down') {
225
- selected = (selected + 1) % items.length;
226
- redraw();
227
- } else if (key.name === 'space') {
228
- const item = items[selected];
229
- const newState = !item.enabled;
230
- const success = onToggle(item, newState);
231
- if (success !== false) {
232
- item.enabled = newState;
233
- }
234
- redraw();
235
- } else if (key.name === 'escape' || key.name === 'return') {
236
- cleanup();
237
- resolve();
238
- }
239
- } catch (err) {
240
- cleanup();
241
- resolve();
242
- }
243
- }
244
-
245
- if (!process.stdin.listenerCount('keypress')) {
246
- readline.emitKeypressEvents(process.stdin);
247
- }
248
-
249
- render();
250
- process.stdin.on('keypress', onKey);
251
- });
252
- }
253
-
254
- module.exports = { pick, pickToggle, isPickerActive };
1
+ /**
2
+ * Interactive arrow-key picker for Banana Code
3
+ *
4
+ * Renders a navigable list in the terminal. Arrow keys move highlight,
5
+ * Enter selects, Escape cancels. Temporarily takes over stdin.
6
+ */
7
+
8
+ const readline = require('readline');
9
+
10
+ const ESC = '\x1b';
11
+ const HIDE_CURSOR = `${ESC}[?25l`;
12
+ const SHOW_CURSOR = `${ESC}[?25h`;
13
+ const CLEAR_LINE = `${ESC}[2K`;
14
+ const MOVE_UP = (n) => n > 0 ? `${ESC}[${n}A` : '';
15
+ const MOVE_COL0 = `${ESC}[0G`;
16
+
17
+ // 256-color codes matching diffViewer.js palette
18
+ const DIM = `${ESC}[2m`;
19
+ const RESET = `${ESC}[0m`;
20
+ const GREEN = `${ESC}[38;5;120m`;
21
+ const YELLOW = `${ESC}[38;5;226m`;
22
+ const CYAN = `${ESC}[38;5;51m`;
23
+ const GRAY = `${ESC}[38;5;245m`;
24
+ const INVERSE = `${ESC}[7m`;
25
+
26
+ // Flag indicating a picker UI is active. When true, the global keypress
27
+ // handler in banana.js should not process keys (arrow keys, escape, etc.)
28
+ // because the picker owns stdin during its lifetime.
29
+ let _pickerActive = false;
30
+ function isPickerActive() { return _pickerActive; }
31
+
32
+ /**
33
+ * Show an interactive picker menu.
34
+ *
35
+ * @param {Array} items - Array of { key, label, description, active, tags }
36
+ * @param {Object} options
37
+ * @param {string} options.title - Header text
38
+ * @param {number} options.selected - Initial selected index (default: active item or 0)
39
+ * @returns {Promise<Object|null>} - Selected item, or null if cancelled
40
+ */
41
+ function pick(items, options = {}) {
42
+ return new Promise((resolve) => {
43
+ if (!items || items.length === 0) {
44
+ resolve(null);
45
+ return;
46
+ }
47
+
48
+ let selected = options.selected ?? items.findIndex(i => i.active);
49
+ if (selected < 0) selected = 0;
50
+
51
+ const out = process.stdout;
52
+ const title = options.title || 'Select:';
53
+ const showVisionIndicator = options.showVisionIndicator === true;
54
+ const footerText = showVisionIndicator
55
+ ? `${DIM} ↑↓ navigate Enter select Esc cancel V=vision${RESET}`
56
+ : `${DIM} ↑↓ navigate Enter select Esc cancel${RESET}`;
57
+
58
+ // Pad key names to align descriptions
59
+ const maxKeyLen = Math.max(...items.map(i => (i.key || '').length));
60
+
61
+ function formatItem(item, index) {
62
+ const isSel = index === selected;
63
+ const isActive = item.active;
64
+ const marker = isActive ? `${GREEN}●${RESET}` : `${GRAY}○${RESET}`;
65
+ const pointer = isSel ? `${CYAN}▸${RESET} ` : ' ';
66
+ const hasVision = Array.isArray(item.tags) && item.tags.includes('vision');
67
+ const vision = showVisionIndicator
68
+ ? (hasVision ? `${CYAN}V${RESET}` : `${DIM}.${RESET}`)
69
+ : '';
70
+ const key = (item.key || '').padEnd(maxKeyLen);
71
+ const tags = item.tags?.length ? ` ${DIM}[${item.tags.join(', ')}]${RESET}` : '';
72
+ const desc = item.description ? ` ${DIM}- ${item.description}${RESET}` : '';
73
+ const label = item.label || item.name || item.key;
74
+
75
+ if (isSel) {
76
+ const lead = showVisionIndicator ? `${pointer}${marker} ${vision} ` : `${pointer}${marker} `;
77
+ return `${lead}${INVERSE} ${YELLOW}${key}${RESET}${INVERSE} ${label} ${RESET}${tags}`;
78
+ }
79
+ const lead = showVisionIndicator ? `${pointer}${marker} ${vision} ` : `${pointer}${marker} `;
80
+ return `${lead}${YELLOW}${key}${RESET} ${DIM}${label}${RESET}${tags}`;
81
+ }
82
+
83
+ // Total lines we render (title + items + footer)
84
+ const totalLines = 1 + items.length + 1;
85
+
86
+ function render() {
87
+ _pickerActive = true;
88
+ let buf = HIDE_CURSOR;
89
+ buf += `\n${CYAN} ${title}${RESET}\n`;
90
+ for (let i = 0; i < items.length; i++) {
91
+ buf += ` ${formatItem(items[i], i)}\n`;
92
+ }
93
+ buf += `${footerText}\n`;
94
+ out.write(buf);
95
+ }
96
+
97
+ function redraw() {
98
+ let buf = MOVE_UP(totalLines) + MOVE_COL0;
99
+ for (let i = 0; i < totalLines; i++) {
100
+ buf += CLEAR_LINE + '\n';
101
+ }
102
+ buf += MOVE_UP(totalLines) + MOVE_COL0;
103
+ buf += `${CYAN} ${title}${RESET}\n`;
104
+ for (let i = 0; i < items.length; i++) {
105
+ buf += ` ${formatItem(items[i], i)}\n`;
106
+ }
107
+ buf += `${footerText}\n`;
108
+ out.write(buf);
109
+ }
110
+
111
+ function cleanup() {
112
+ _pickerActive = false;
113
+ process.stdin.removeListener('keypress', onKey);
114
+ out.write(SHOW_CURSOR);
115
+ }
116
+
117
+ function onKey(str, key) {
118
+ if (!key) return;
119
+ try {
120
+ if (key.name === 'up') {
121
+ selected = (selected - 1 + items.length) % items.length;
122
+ redraw();
123
+ } else if (key.name === 'down') {
124
+ selected = (selected + 1) % items.length;
125
+ redraw();
126
+ } else if (key.name === 'return') {
127
+ cleanup();
128
+ resolve(items[selected]);
129
+ } else if (key.name === 'escape') {
130
+ cleanup();
131
+ resolve(null);
132
+ }
133
+ } catch (err) {
134
+ cleanup();
135
+ resolve(null);
136
+ }
137
+ }
138
+
139
+ // Ensure keypress events are flowing
140
+ if (!process.stdin.listenerCount('keypress')) {
141
+ readline.emitKeypressEvents(process.stdin);
142
+ }
143
+
144
+ render();
145
+ process.stdin.on('keypress', onKey);
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Interactive toggle list. Items can be toggled on/off with Space.
151
+ * Arrow keys navigate, Space toggles, Escape exits.
152
+ *
153
+ * @param {Array} items - Array of { key, label, description, enabled, meta }
154
+ * @param {Object} options
155
+ * @param {string} options.title - Header text
156
+ * @param {Function} options.onToggle - Called with (item, newEnabled) when toggled. Should return true if toggle succeeded.
157
+ * @returns {Promise<void>} - Resolves when user presses Escape
158
+ */
159
+ function pickToggle(items, options = {}) {
160
+ return new Promise((resolve) => {
161
+ if (!items || items.length === 0) {
162
+ resolve();
163
+ return;
164
+ }
165
+
166
+ let selected = 0;
167
+ const out = process.stdout;
168
+ const title = options.title || 'Toggle:';
169
+ const onToggle = options.onToggle || (() => true);
170
+ const RED = `${ESC}[38;5;203m`;
171
+ const footerText = `${DIM} ↑↓ navigate Space toggle Enter/Esc done${RESET}`;
172
+
173
+ const maxKeyLen = Math.max(...items.map(i => (i.key || '').length));
174
+
175
+ function formatItem(item, index) {
176
+ const isSel = index === selected;
177
+ const pointer = isSel ? `${CYAN}▸${RESET} ` : ' ';
178
+ const toggle = item.enabled ? `${GREEN}on ${RESET}` : `${RED}off${RESET}`;
179
+ const key = (item.key || '').padEnd(maxKeyLen);
180
+ const desc = item.description ? ` ${DIM}${item.description}${RESET}` : '';
181
+ const meta = item.meta ? ` ${DIM}[${item.meta}]${RESET}` : '';
182
+
183
+ if (isSel) {
184
+ return `${pointer}[${toggle}] ${INVERSE} ${YELLOW}${key}${RESET}${INVERSE} ${RESET}${desc}${meta}`;
185
+ }
186
+ return `${pointer}[${toggle}] ${YELLOW}${key}${RESET}${desc}${meta}`;
187
+ }
188
+
189
+ const totalLines = 1 + items.length + 1;
190
+
191
+ function render() {
192
+ _pickerActive = true;
193
+ let buf = HIDE_CURSOR;
194
+ buf += `\n${CYAN} ${title}${RESET}\n`;
195
+ for (let i = 0; i < items.length; i++) {
196
+ buf += ` ${formatItem(items[i], i)}\n`;
197
+ }
198
+ buf += `${footerText}\n`;
199
+ out.write(buf);
200
+ }
201
+
202
+ function redraw() {
203
+ let buf = MOVE_UP(totalLines) + MOVE_COL0;
204
+ for (let i = 0; i < totalLines; i++) {
205
+ buf += CLEAR_LINE + '\n';
206
+ }
207
+ buf += MOVE_UP(totalLines) + MOVE_COL0;
208
+ buf += `${CYAN} ${title}${RESET}\n`;
209
+ for (let i = 0; i < items.length; i++) {
210
+ buf += ` ${formatItem(items[i], i)}\n`;
211
+ }
212
+ buf += `${footerText}\n`;
213
+ out.write(buf);
214
+ }
215
+
216
+ function cleanup() {
217
+ _pickerActive = false;
218
+ process.stdin.removeListener('keypress', onKey);
219
+ out.write(SHOW_CURSOR);
220
+ }
221
+
222
+ function onKey(str, key) {
223
+ if (!key) return;
224
+ try {
225
+ if (key.name === 'up') {
226
+ selected = (selected - 1 + items.length) % items.length;
227
+ redraw();
228
+ } else if (key.name === 'down') {
229
+ selected = (selected + 1) % items.length;
230
+ redraw();
231
+ } else if (key.name === 'space') {
232
+ const item = items[selected];
233
+ const newState = !item.enabled;
234
+ const success = onToggle(item, newState);
235
+ if (success !== false) {
236
+ item.enabled = newState;
237
+ }
238
+ redraw();
239
+ } else if (key.name === 'escape' || key.name === 'return') {
240
+ cleanup();
241
+ resolve();
242
+ }
243
+ } catch (err) {
244
+ cleanup();
245
+ resolve();
246
+ }
247
+ }
248
+
249
+ if (!process.stdin.listenerCount('keypress')) {
250
+ readline.emitKeypressEvents(process.stdin);
251
+ }
252
+
253
+ render();
254
+ process.stdin.on('keypress', onKey);
255
+ });
256
+ }
257
+
258
+ module.exports = { pick, pickToggle, isPickerActive };
package/lib/lmStudio.js CHANGED
@@ -28,6 +28,7 @@ class LmStudio {
28
28
  if (options.tools) {
29
29
  body.tools = options.tools;
30
30
  body.tool_choice = options.toolChoice ?? 'auto';
31
+ body.parallel_tool_calls = false; // Prevent local models from spamming dozens of tool calls
31
32
  }
32
33
  if (typeof options.thinking === 'number' && options.thinking > 0) {
33
34
  body.thinking = { type: 'enabled', budget_tokens: options.thinking };
@@ -143,7 +144,7 @@ class LmStudio {
143
144
 
144
145
  /**
145
146
  * Get all loaded model instances from LM Studio REST API.
146
- * Returns array of { key, instanceId, displayName }
147
+ * Returns array of { key, instanceId, displayName, contextLength }
147
148
  */
148
149
  async getLoadedInstances() {
149
150
  try {
@@ -155,7 +156,12 @@ class LmStudio {
155
156
  const instances = [];
156
157
  for (const model of data.models || []) {
157
158
  for (const inst of model.loaded_instances || []) {
158
- instances.push({ key: model.key, instanceId: inst.id, displayName: model.display_name });
159
+ instances.push({
160
+ key: model.key,
161
+ instanceId: inst.id,
162
+ displayName: model.display_name || model.key,
163
+ contextLength: inst.context_length || null
164
+ });
159
165
  }
160
166
  }
161
167
  return instances;
package/lib/mcpClient.js CHANGED
@@ -76,7 +76,7 @@ class McpClient {
76
76
  params: {
77
77
  protocolVersion: '2024-11-05',
78
78
  capabilities: {},
79
- clientInfo: { name: 'banana', version: '1.2.0' }
79
+ clientInfo: { name: 'banana', version: '1.3.0' }
80
80
  }
81
81
  }),
82
82
  signal: AbortSignal.timeout(this.timeout)
@@ -215,66 +215,52 @@ class ModelRegistry {
215
215
  }
216
216
 
217
217
  /**
218
- * Auto-discover local model IDs from LM Studio and update local registry
218
+ * Probe LM Studio and inject/update a dynamic "lmstudio" entry in the registry.
219
+ * If LM Studio is not connected or has no model loaded, removes the entry.
219
220
  */
220
- async discover() {
221
- if (!this.lmStudio) return { matched: 0, total: 0 };
221
+ async refreshLmStudio() {
222
+ if (!this.lmStudio) return;
222
223
 
223
- const lmModels = await this.lmStudio.listModels();
224
- if (lmModels.length === 0) return { matched: 0, total: 0 };
225
-
226
- let matched = 0;
227
- const usedLmIds = new Set();
228
- for (const [key, model] of Object.entries(this.localRegistry.models)) {
229
- if ((model.provider || 'local') !== 'local') continue;
230
-
231
- // Do not overwrite explicit model IDs from models.json.
232
- const existingId = String(model.id || '').trim();
233
- if (existingId) continue;
234
-
235
- const keywords = this._getMatchKeywords(key, model.name);
236
- for (const lmModel of lmModels) {
237
- const lmId = (lmModel.id || '').toLowerCase();
238
- if (!lmId || usedLmIds.has(lmId)) continue;
239
- if (keywords.some(kw => lmId.includes(kw))) {
240
- model.id = lmModel.id;
241
- usedLmIds.add(lmId);
242
- matched++;
243
- break;
244
- }
245
- }
224
+ const connected = await this.lmStudio.isConnected();
225
+ if (!connected) {
226
+ delete this.registry.models['lmstudio'];
227
+ return;
246
228
  }
247
229
 
248
- if (matched > 0) {
249
- this.save();
250
- this.refreshRemoteModels();
230
+ const instances = await this.lmStudio.getLoadedInstances();
231
+ const first = instances[0];
232
+
233
+ if (first && first.key) {
234
+ this.registry.models['lmstudio'] = {
235
+ name: `LM Studio: ${first.displayName}`,
236
+ id: first.key,
237
+ provider: 'local',
238
+ contextLimit: first.contextLength || 32768,
239
+ maxOutputTokens: Math.min(first.contextLength || 32768, 32768),
240
+ supportsThinking: false,
241
+ prompt: 'code-agent',
242
+ inferenceSettings: { temperature: 0.7, topP: 0.9 },
243
+ tags: ['local', 'lm-studio'],
244
+ tier: 'local',
245
+ description: `${first.displayName} via LM Studio`,
246
+ dynamic: true
247
+ };
248
+ } else {
249
+ // Connected but no model loaded
250
+ this.registry.models['lmstudio'] = {
251
+ name: 'LM Studio (no model loaded)',
252
+ id: '',
253
+ provider: 'local',
254
+ contextLimit: 32768,
255
+ supportsThinking: false,
256
+ prompt: 'code-agent',
257
+ inferenceSettings: { temperature: 0.7, topP: 0.9 },
258
+ tags: ['local', 'lm-studio'],
259
+ tier: 'local',
260
+ description: 'Load a model in LM Studio to use',
261
+ dynamic: true
262
+ };
251
263
  }
252
-
253
- return { matched, total: lmModels.length };
254
- }
255
-
256
- /**
257
- * Generate match keywords from local model key and name
258
- */
259
- _getMatchKeywords(key, name) {
260
- const keywords = [];
261
- const keyMap = {
262
- 'silverback': ['silverback', 'gpt_oss'],
263
- 'qwen35': ['qwen3.5-35b', 'qwen3.5-35'],
264
- 'nemotron': ['nemotron'],
265
- 'coder': ['qwen3-coder-30b', 'qwen3-coder-30'],
266
- 'glm': ['glm-4.7', 'glm-4-flash', 'glm4'],
267
- 'max': ['qwen3-coder-next', 'coder-next'],
268
- 'mistral': ['mistral-small']
269
- };
270
- if (keyMap[key]) keywords.push(...keyMap[key]);
271
-
272
- if (typeof name === 'string' && name.trim()) {
273
- const parts = name.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
274
- keywords.push(...parts.slice(0, 3));
275
- }
276
-
277
- return [...new Set(keywords)];
278
264
  }
279
265
 
280
266
  /**
@@ -8,7 +8,7 @@ const { AgenticRunner, TOOLS, READ_ONLY_TOOLS } = require('./agenticRunner');
8
8
  const crypto = require('crypto');
9
9
 
10
10
  const MAX_DEPTH = 2;
11
- const DEFAULT_MAX_ITERATIONS_SUBAGENT = 25;
11
+ const DEFAULT_MAX_ITERATIONS_SUBAGENT = 40;
12
12
  const DEFAULT_TIMEOUT_CLOUD_MS = 120_000;
13
13
  const DEFAULT_TIMEOUT_LOCAL_MS = 300_000;
14
14
  const DEFAULT_MAX_TOKENS_SUBAGENT = 16384;
package/models.json CHANGED
@@ -27,19 +27,6 @@
27
27
  "description": "Balanced. Everyday coding, solid quality + speed.",
28
28
  "provider": "monkey"
29
29
  },
30
- "gibbon": {
31
- "name": "Gibbon",
32
- "id": "gibbon",
33
- "contextLimit": 262000,
34
- "maxOutputTokens": 65536,
35
- "supportsThinking": false,
36
- "prompt": "code-agent",
37
- "inferenceSettings": { "temperature": 0.7, "topP": 0.9 },
38
- "tags": ["fast", "quick-fixes", "tool-calling", "vision"],
39
- "tier": "gibbon",
40
- "description": "Fast. Quick fixes, autocomplete, rapid iteration.",
41
- "provider": "monkey"
42
- },
43
30
  "tamarin": {
44
31
  "name": "Tamarin",
45
32
  "id": "tamarin",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "banana-code",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "AI coding agent CLI powered by Monkey Models and remote providers (Anthropic, OpenAI, OpenRouter)",
5
5
  "main": "banana.js",
6
6
  "bin": {
@@ -37,6 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "diff": "^8.0.3",
40
- "glob": "^13.0.6"
40
+ "glob": "^13.0.6",
41
+ "update-notifier": "^7.3.1"
41
42
  }
42
43
  }