banana-code 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/banana.js +73 -42
- package/lib/agenticRunner.js +42 -12
- package/lib/interactivePicker.js +258 -254
- package/lib/lmStudio.js +8 -2
- package/lib/mcpClient.js +1 -1
- package/lib/modelRegistry.js +41 -55
- package/models.json +0 -13
- package/package.json +3 -2
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');
|
|
@@ -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 ||
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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.
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
2798
|
-
//
|
|
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
|
-
//
|
|
3562
|
-
//
|
|
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
|
-
//
|
|
3928
|
-
|
|
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
|
}
|
package/lib/agenticRunner.js
CHANGED
|
@@ -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: 8, escalate: 10, hardStop: 12 }
|
|
1314
|
+
: (modelId.includes('tamarin'))
|
|
1315
|
+
? { nudge: 3, escalate: 5, hardStop: 6 }
|
|
1316
|
+
: /* mandrill, local, openrouter, or unknown */ { nudge: 5, escalate: 7, hardStop: 10 };
|
|
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
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
|
1675
|
-
if (readOnlyStreak ===
|
|
1676
|
-
appendDebugLog(` [READ-ONLY NUDGE] ${readOnlyStreak} consecutive read-only iterations\n`);
|
|
1677
|
-
this.onWarning(
|
|
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:
|
|
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
|
|
1742
|
-
if (readOnlyStreak >=
|
|
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({
|
package/lib/interactivePicker.js
CHANGED
|
@@ -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 }
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
buf
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
buf
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
buf += `${
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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({
|
|
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
package/lib/modelRegistry.js
CHANGED
|
@@ -215,66 +215,52 @@ class ModelRegistry {
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
/**
|
|
218
|
-
*
|
|
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
|
|
221
|
-
if (!this.lmStudio) return
|
|
221
|
+
async refreshLmStudio() {
|
|
222
|
+
if (!this.lmStudio) return;
|
|
222
223
|
|
|
223
|
-
const
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
/**
|
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.
|
|
3
|
+
"version": "1.3.0",
|
|
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
|
}
|