free-coding-models 0.3.51 → 0.3.54
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/CHANGELOG.md +18 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/app.js +27 -8
- package/src/command-palette.js +2 -0
- package/src/constants.js +10 -2
- package/src/endpoint-installer.js +1 -1
- package/src/installed-models-manager.js +53 -1
- package/src/key-handler.js +26 -1
- package/src/kilo-config.js +43 -0
- package/src/kilo.js +200 -0
- package/src/opencode.js +56 -0
- package/src/render-table.js +209 -127
- package/src/shell-env.js +3 -0
- package/src/tool-bootstrap.js +10 -0
- package/src/tool-metadata.js +7 -2
- package/src/updater.js +2 -0
- package/src/utils.js +7 -3
- package/web/dist/assets/{index-CJjHD8Iz.js → index-D2ban2S-.js} +1 -1
- package/web/dist/index.html +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## [0.3.54] - 2026-04-18
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **Improved OpenCode WebUI Launch** — The `--opencode-web` flag now correctly spawns the `opencode web` command, providing a integrated browser-based coding experience with pre-configured model selection.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Kilo CLI Support** — Added `--kilo` flag to launch the Kilo CLI with the selected model. Kilo is a fork of OpenCode and shares the same configuration structure (stored in `~/.config/kilo/opencode.json`).
|
|
10
|
+
|
|
11
|
+
## [0.3.53] - 2026-04-18
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **OpenCode WebUI Support** — Added `--opencode-web` flag to open the OpenCode WebUI dashboard after configuring the selected model.
|
|
16
|
+
|
|
17
|
+
## [0.3.52] - 2026-04-18
|
|
18
|
+
|
|
1
19
|
## [0.3.51] - 2026-04-11
|
|
2
20
|
|
|
3
21
|
### Changed
|
package/README.md
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
13
|
<h1 align="center">free-coding-models</h1>
|
|
14
|
+
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
<p align="center">
|
|
16
18
|
<strong>Find the fastest free coding model in seconds</strong><br>
|
|
17
|
-
<sub>Ping 238 models across 25 AI Free providers in real-time </sub><br> <sub> Install Free API endpoints to your favorite AI coding tool: <br>📦 OpenCode, 🦞 OpenClaw, 💘 Crush, 🪿 Goose, 🛠 Aider, 🐉 Qwen Code, 🤲 OpenHands, ⚡ Amp, 🔮 Hermes, ▶️ Continue, 🧠 Cline, 🛠️ Xcode, π Pi, 🦘 Rovo or ♊ Gemini in one keystroke</sub>
|
|
19
|
+
<sub>Ping 238 models across 25 AI Free providers in real-time </sub><br> <sub> Install Free API endpoints to your favorite AI coding tool: <br>📦 OpenCode, 📦 OpenCode Desktop, 📦 OpenCode WebUI, 🦞 OpenClaw, 💘 Crush, 🪿 Goose, 🛠 Aider, ⚡️ Kilo CLI, 🐉 Qwen Code, 🤲 OpenHands, ⚡ Amp, 🔮 Hermes, ▶️ Continue, 🧠 Cline, 🛠️ Xcode, π Pi, 🦘 Rovo or ♊ Gemini in one keystroke</sub>
|
|
18
20
|
</p>
|
|
19
21
|
|
|
20
22
|
|
|
@@ -260,10 +262,12 @@ When launching the web dashboard, `free-coding-models` prefers `http://localhost
|
|
|
260
262
|
|------|----------|
|
|
261
263
|
| `--opencode` | 📦 OpenCode CLI |
|
|
262
264
|
| `--opencode-desktop` | 📦 OpenCode Desktop |
|
|
265
|
+
| `--opencode-web` | 📦 OpenCode WebUI |
|
|
263
266
|
| `--openclaw` | 🦞 OpenClaw |
|
|
264
267
|
| `--crush` | 💘 Crush |
|
|
265
268
|
| `--goose` | 🪿 Goose |
|
|
266
269
|
| `--aider` | 🛠 Aider |
|
|
270
|
+
| `--kilo` | ⚡️ Kilo CLI |
|
|
267
271
|
| `--qwen` | 🐉 Qwen Code |
|
|
268
272
|
| `--openhands` | 🤲 OpenHands |
|
|
269
273
|
| `--amp` | ⚡ Amp |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.54",
|
|
4
4
|
"description": "Find the fastest coding LLM models in seconds — ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nvidia",
|
package/src/app.js
CHANGED
|
@@ -105,7 +105,7 @@ import { usageForRow as _usageForRow } from '../src/usage-reader.js'
|
|
|
105
105
|
import { buildProviderModelTokenKey, loadTokenUsageByProviderModel } from '../src/token-usage-reader.js'
|
|
106
106
|
import { parseOpenRouterResponse, fetchProviderQuota as _fetchProviderQuotaFromModule } from '../src/provider-quota-fetchers.js'
|
|
107
107
|
import { isKnownQuotaTelemetry } from '../src/quota-capabilities.js'
|
|
108
|
-
import { ALT_ENTER, ALT_LEAVE, ALT_HOME, PING_TIMEOUT, PING_INTERVAL, FPS, COL_MODEL, COL_MS, CELL_W, FRAMES, TIER_CYCLE, SETTINGS_OVERLAY_BG, HELP_OVERLAY_BG, RECOMMEND_OVERLAY_BG, OVERLAY_PANEL_WIDTH, TABLE_HEADER_LINES, TABLE_FOOTER_LINES, TABLE_FIXED_LINES, WIDTH_WARNING_MIN_COLS, msCell, spinCell } from '../src/constants.js'
|
|
108
|
+
import { ALT_ENTER, ALT_LEAVE, ALT_HOME, PING_TIMEOUT, PING_INTERVAL, FPS, COL_MODEL, COL_MS, CELL_W, FRAMES, TIER_CYCLE, VERDICT_CYCLE, HEALTH_CYCLE, SETTINGS_OVERLAY_BG, HELP_OVERLAY_BG, RECOMMEND_OVERLAY_BG, OVERLAY_PANEL_WIDTH, TABLE_HEADER_LINES, TABLE_FOOTER_LINES, TABLE_FIXED_LINES, WIDTH_WARNING_MIN_COLS, msCell, spinCell } from '../src/constants.js'
|
|
109
109
|
import { TIER_COLOR } from '../src/tier-colors.js'
|
|
110
110
|
import { resolveCloudflareUrl, buildPingRequest, ping, extractQuotaPercent, getProviderQuotaPercentCached, usagePlaceholderForProvider } from '../src/ping.js'
|
|
111
111
|
import { runFiableMode, filterByTierOrExit, fetchOpenRouterFreeModels } from '../src/analysis.js'
|
|
@@ -117,7 +117,8 @@ import { promptApiKey } from '../src/setup.js'
|
|
|
117
117
|
import { syncShellEnv, ensureShellRcSource, promptShellEnvMigration, removeShellEnv } from '../src/shell-env.js'
|
|
118
118
|
import { stripAnsi, maskApiKey, displayWidth, padEndDisplay, tintOverlayLines, keepOverlayTargetVisible, sliceOverlayLines, calculateViewport, sortResultsWithPinnedFavorites, adjustScrollOffset } from '../src/render-helpers.js'
|
|
119
119
|
import { renderTable, PROVIDER_COLOR } from '../src/render-table.js'
|
|
120
|
-
import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop } from '../src/opencode.js'
|
|
120
|
+
import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop, startOpenCodeWeb } from '../src/opencode.js'
|
|
121
|
+
import { startKilo } from '../src/kilo.js'
|
|
121
122
|
import { startOpenClaw } from '../src/openclaw.js'
|
|
122
123
|
import { createOverlayRenderers } from '../src/overlays.js'
|
|
123
124
|
import { createKeyHandler, createMouseEventHandler } from '../src/key-handler.js'
|
|
@@ -252,10 +253,12 @@ export async function runApp(cliArgs, config) {
|
|
|
252
253
|
const flagByMode = {
|
|
253
254
|
opencode: cliArgs.openCodeMode,
|
|
254
255
|
'opencode-desktop': cliArgs.openCodeDesktopMode,
|
|
256
|
+
'opencode-web': cliArgs.openCodeWebMode,
|
|
255
257
|
openclaw: cliArgs.openClawMode,
|
|
256
258
|
aider: cliArgs.aiderMode,
|
|
257
259
|
crush: cliArgs.crushMode,
|
|
258
260
|
goose: cliArgs.gooseMode,
|
|
261
|
+
kilo: cliArgs.kiloMode,
|
|
259
262
|
qwen: cliArgs.qwenMode,
|
|
260
263
|
openhands: cliArgs.openHandsMode,
|
|
261
264
|
amp: cliArgs.ampMode,
|
|
@@ -423,6 +426,8 @@ export async function runApp(cliArgs, config) {
|
|
|
423
426
|
mode, // 📖 'opencode' or 'openclaw' — controls Enter action
|
|
424
427
|
tierFilterMode: 0, // 📖 Index into TIER_CYCLE (0=All, 1=S+, 2=S, ...)
|
|
425
428
|
originFilterMode: 0, // 📖 Index into ORIGIN_CYCLE (0=All, then providers)
|
|
429
|
+
verdictFilterMode: 0, // 📖 Index into VERDICT_CYCLE (0=All, then verdicts)
|
|
430
|
+
healthFilterMode: 0, // 📖 Index into HEALTH_CYCLE (0=All, then health states)
|
|
426
431
|
hideUnconfiguredModels: config.settings?.hideUnconfiguredModels === true, // 📖 Hide providers with no configured API key when true.
|
|
427
432
|
favoritesPinnedAndSticky: config.settings?.favoritesPinnedAndSticky === true, // 📖 false by default: favorites follow normal sort/filter rules until Y enables pinned+sticky mode.
|
|
428
433
|
footerHidden: config.settings?.footerHidden === true, // 📖 true = footer is collapsed to a single toggle hint
|
|
@@ -739,6 +744,8 @@ export async function runApp(cliArgs, config) {
|
|
|
739
744
|
function applyTierFilter() {
|
|
740
745
|
const activeTier = TIER_CYCLE[state.tierFilterMode]
|
|
741
746
|
const activeOrigin = ORIGIN_CYCLE[state.originFilterMode]
|
|
747
|
+
const activeVerdict = VERDICT_CYCLE[state.verdictFilterMode]
|
|
748
|
+
const activeHealth = HEALTH_CYCLE[state.healthFilterMode]
|
|
742
749
|
state.results.forEach(r => {
|
|
743
750
|
// 📖 Sticky-favorites mode keeps favorites visible regardless of configured-only, tier, or provider filters.
|
|
744
751
|
if (state.favoritesPinnedAndSticky && r.isFavorite) {
|
|
@@ -754,12 +761,16 @@ export async function runApp(cliArgs, config) {
|
|
|
754
761
|
r.hidden = true
|
|
755
762
|
return
|
|
756
763
|
}
|
|
757
|
-
// 📖 Apply
|
|
758
|
-
// 📖 TIER_LETTER_MAP is used so --tier S also includes S+ models (tier family behavior).
|
|
764
|
+
// 📖 Apply tier, origin, verdict, and health filters — model is hidden if it fails any
|
|
759
765
|
const allowedTiers = (activeTier && TIER_LETTER_MAP[activeTier]) ? TIER_LETTER_MAP[activeTier] : [activeTier]
|
|
760
766
|
const tierHide = activeTier !== null && !allowedTiers.includes(r.tier)
|
|
761
767
|
const originHide = activeOrigin !== null && r.providerKey !== activeOrigin
|
|
762
|
-
|
|
768
|
+
// 📖 Verdict filter: match against getVerdict(r) when active
|
|
769
|
+
const rVerdict = getVerdict(r)
|
|
770
|
+
const verdictHide = activeVerdict !== null && rVerdict !== activeVerdict
|
|
771
|
+
// 📖 Health filter: match against r.status when active
|
|
772
|
+
const healthHide = activeHealth !== null && r.status !== activeHealth
|
|
773
|
+
if (tierHide || originHide || verdictHide || healthHide) {
|
|
763
774
|
r.hidden = true
|
|
764
775
|
return
|
|
765
776
|
}
|
|
@@ -864,6 +875,8 @@ export async function runApp(cliArgs, config) {
|
|
|
864
875
|
runUpdate,
|
|
865
876
|
startOpenClaw,
|
|
866
877
|
startOpenCodeDesktop,
|
|
878
|
+
startOpenCodeWeb,
|
|
879
|
+
startKilo,
|
|
867
880
|
startOpenCode,
|
|
868
881
|
startExternalTool,
|
|
869
882
|
getToolModeOrder,
|
|
@@ -1042,7 +1055,10 @@ export async function runApp(cliArgs, config) {
|
|
|
1042
1055
|
state.versionAlertsEnabled,
|
|
1043
1056
|
state.favoritesPinnedAndSticky,
|
|
1044
1057
|
state.customTextFilter,
|
|
1045
|
-
state.lastReleaseDate
|
|
1058
|
+
state.lastReleaseDate,
|
|
1059
|
+
state.footerHidden,
|
|
1060
|
+
state.verdictFilterMode,
|
|
1061
|
+
state.healthFilterMode
|
|
1046
1062
|
)
|
|
1047
1063
|
}
|
|
1048
1064
|
tableContent = state.commandPaletteFrozenTable
|
|
@@ -1077,7 +1093,10 @@ export async function runApp(cliArgs, config) {
|
|
|
1077
1093
|
state.versionAlertsEnabled,
|
|
1078
1094
|
state.favoritesPinnedAndSticky,
|
|
1079
1095
|
state.customTextFilter,
|
|
1080
|
-
state.lastReleaseDate
|
|
1096
|
+
state.lastReleaseDate,
|
|
1097
|
+
state.footerHidden,
|
|
1098
|
+
state.verdictFilterMode,
|
|
1099
|
+
state.healthFilterMode
|
|
1081
1100
|
)
|
|
1082
1101
|
}
|
|
1083
1102
|
|
|
@@ -1121,7 +1140,7 @@ export async function runApp(cliArgs, config) {
|
|
|
1121
1140
|
pinFavorites: state.favoritesPinnedAndSticky,
|
|
1122
1141
|
})
|
|
1123
1142
|
|
|
1124
|
-
process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate, state.footerHidden))
|
|
1143
|
+
process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate, state.footerHidden, state.verdictFilterMode, state.healthFilterMode))
|
|
1125
1144
|
if (process.stdout.isTTY) {
|
|
1126
1145
|
process.stdout.flush && process.stdout.flush()
|
|
1127
1146
|
}
|
package/src/command-palette.js
CHANGED
|
@@ -20,11 +20,13 @@ import { TOOL_METADATA, TOOL_MODE_ORDER } from './tool-metadata.js'
|
|
|
20
20
|
const TOOL_MODE_DESCRIPTIONS = {
|
|
21
21
|
opencode: 'Launch in OpenCode CLI with the selected model.',
|
|
22
22
|
'opencode-desktop': 'Set model in shared config, then open OpenCode Desktop.',
|
|
23
|
+
'opencode-web': 'Set model in shared config, then open OpenCode WebUI.',
|
|
23
24
|
openclaw: 'Set default model in OpenClaw and launch it.',
|
|
24
25
|
crush: 'Launch Crush with this provider/model pair.',
|
|
25
26
|
goose: 'Launch Goose and preselect the active model.',
|
|
26
27
|
pi: 'Launch Pi with model/provider flags.',
|
|
27
28
|
aider: 'Launch Aider configured on the selected model.',
|
|
29
|
+
kilo: 'Set model in shared config, then launch Kilo CLI.',
|
|
28
30
|
qwen: 'Launch Qwen Code using the selected provider model.',
|
|
29
31
|
openhands: 'Launch OpenHands with the selected model endpoint.',
|
|
30
32
|
amp: 'Launch Amp with this model as active target.',
|
package/src/constants.js
CHANGED
|
@@ -77,6 +77,14 @@ export const FRAMES = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','
|
|
|
77
77
|
// 📖 Index 0 = no filter (show all), then each tier name in descending quality order.
|
|
78
78
|
export const TIER_CYCLE = [null, 'S+', 'S', 'A+', 'A', 'A-', 'B+', 'B', 'C']
|
|
79
79
|
|
|
80
|
+
// 📖 VERDICT_CYCLE: cycles through health verdict labels (0=All, then each verdict).
|
|
81
|
+
// 📖 Based on VERDICT_ORDER from utils.js — includes all possible getVerdict() values.
|
|
82
|
+
export const VERDICT_CYCLE = [null, 'Perfect', 'Normal', 'Slow', 'Spiky', 'Very Slow', 'Overloaded', 'Unstable', 'Not Active', 'Pending']
|
|
83
|
+
|
|
84
|
+
// 📖 HEALTH_CYCLE: cycles through ping status states (0=All, then each status).
|
|
85
|
+
// 📖 Based on the status values in the app: up, timeout, down, auth_error, noauth, pending.
|
|
86
|
+
export const HEALTH_CYCLE = [null, 'up', 'timeout', 'down', 'auth_error', 'noauth', 'pending']
|
|
87
|
+
|
|
80
88
|
// 📖 Overlay background chalk functions — each overlay panel has a distinct tint
|
|
81
89
|
// 📖 so users can tell Settings, Help, Recommend, and Log panels apart at a glance.
|
|
82
90
|
export const SETTINGS_OVERLAY_BG = chalk.bgRgb(0, 0, 0)
|
|
@@ -93,8 +101,8 @@ export const WIDTH_WARNING_MIN_COLS = 80
|
|
|
93
101
|
|
|
94
102
|
// 📖 Table row-budget constants — must stay in sync with renderTable()'s actual output.
|
|
95
103
|
// 📖 If this drifts, model rows overflow and can push the title row out of view.
|
|
96
|
-
export const TABLE_HEADER_LINES =
|
|
97
|
-
export const TABLE_FOOTER_LINES =
|
|
104
|
+
export const TABLE_HEADER_LINES = 5 // 📖 title, filter bar, spacer, column headers, separator
|
|
105
|
+
export const TABLE_FOOTER_LINES = 1 // 📖 single toggle-hint line when collapsed, full footer otherwise
|
|
98
106
|
export const TABLE_FIXED_LINES = TABLE_HEADER_LINES + TABLE_FOOTER_LINES
|
|
99
107
|
|
|
100
108
|
// ─── Small cell-formatting helpers ────────────────────────────────────────────
|
|
@@ -52,7 +52,7 @@ import { getToolMeta } from './tool-metadata.js'
|
|
|
52
52
|
const DIRECT_INSTALL_UNSUPPORTED_PROVIDERS = new Set(['replicate', 'zai', 'rovo', 'gemini', 'opencode-zen'])
|
|
53
53
|
// 📖 Install Endpoints only lists tools whose persisted config shape is actually supported here.
|
|
54
54
|
// 📖 Claude Code, Codex, and Gemini stay out while their dedicated bridges are being rebuilt.
|
|
55
|
-
const INSTALL_TARGET_MODES = ['opencode', 'opencode-desktop', 'openclaw', 'crush', 'goose', 'pi', 'aider', 'qwen', 'openhands', 'amp', 'hermes', 'continue', 'cline']
|
|
55
|
+
const INSTALL_TARGET_MODES = ['opencode', 'opencode-desktop', 'opencode-web', 'openclaw', 'kilo', 'crush', 'goose', 'pi', 'aider', 'qwen', 'openhands', 'amp', 'hermes', 'continue', 'cline']
|
|
56
56
|
|
|
57
57
|
function getDefaultPaths() {
|
|
58
58
|
const home = homedir()
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* - Goose (~/.config/goose/config.yaml + custom_providers/*.json)
|
|
15
15
|
* - Crush (~/.config/crush/crush.json)
|
|
16
16
|
* - Aider (~/.aider.conf.yml)
|
|
17
|
+
* - Kilo (~/.config/kilo/opencode.json)
|
|
17
18
|
* - Qwen (~/.qwen/settings.json)
|
|
18
19
|
* - Pi (~/.pi/agent/models.json + settings.json)
|
|
19
20
|
* - OpenHands (~/.fcm-openhands-env)
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
* → parseGooseConfig — Parse Goose YAML config
|
|
30
31
|
* → parseCrushConfig — Parse Crush JSON config
|
|
31
32
|
* → parseAiderConfig — Parse Aider YAML config
|
|
33
|
+
* → parseKiloConfig — Parse Kilo JSON config
|
|
32
34
|
* → parseQwenConfig — Parse Qwen JSON config
|
|
33
35
|
* → parsePiConfig — Parse Pi JSON configs
|
|
34
36
|
* → parseOpenHandsConfig — Parse OpenHands env file
|
|
@@ -58,6 +60,7 @@ function getToolConfigPaths(homeDir = homedir()) {
|
|
|
58
60
|
goose: join(homeDir, '.config', 'goose', 'config.yaml'),
|
|
59
61
|
crush: join(homeDir, '.config', 'crush', 'crush.json'),
|
|
60
62
|
aider: join(homeDir, '.aider.conf.yml'),
|
|
63
|
+
kilo: join(homeDir, '.config', 'kilo', 'opencode.json'),
|
|
61
64
|
qwen: join(homeDir, '.qwen', 'settings.json'),
|
|
62
65
|
piModels: join(homeDir, '.pi', 'agent', 'models.json'),
|
|
63
66
|
piSettings: join(homeDir, '.pi', 'agent', 'settings.json'),
|
|
@@ -270,6 +273,43 @@ function parseAiderConfig(paths = getToolConfigPaths()) {
|
|
|
270
273
|
}
|
|
271
274
|
}
|
|
272
275
|
|
|
276
|
+
/**
|
|
277
|
+
* 📖 Parse Kilo config for model
|
|
278
|
+
*/
|
|
279
|
+
function parseKiloConfig(paths = getToolConfigPaths()) {
|
|
280
|
+
const configPath = paths.kilo
|
|
281
|
+
if (!existsSync(configPath)) {
|
|
282
|
+
return { isValid: false, models: [], configPath }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const content = readFileSync(configPath, 'utf8')
|
|
287
|
+
const config = JSON.parse(content)
|
|
288
|
+
|
|
289
|
+
const models = []
|
|
290
|
+
if (config.model) {
|
|
291
|
+
models.push({
|
|
292
|
+
modelId: config.model,
|
|
293
|
+
label: config.model,
|
|
294
|
+
tier: '-',
|
|
295
|
+
sweScore: '-',
|
|
296
|
+
providerKey: 'external',
|
|
297
|
+
isExternal: true,
|
|
298
|
+
canLaunch: true,
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
isValid: true,
|
|
304
|
+
hasManagedMarker: true, // Kilo CLI integration always uses modelRef format
|
|
305
|
+
models,
|
|
306
|
+
configPath,
|
|
307
|
+
}
|
|
308
|
+
} catch (err) {
|
|
309
|
+
return { isValid: false, models: [], configPath }
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
273
313
|
/**
|
|
274
314
|
* 📖 Parse Qwen config for model
|
|
275
315
|
*/
|
|
@@ -459,6 +499,8 @@ export function parseToolConfig(toolMode, paths = getToolConfigPaths()) {
|
|
|
459
499
|
return parseCrushConfig(paths)
|
|
460
500
|
case 'aider':
|
|
461
501
|
return parseAiderConfig(paths)
|
|
502
|
+
case 'kilo':
|
|
503
|
+
return parseKiloConfig(paths)
|
|
462
504
|
case 'qwen':
|
|
463
505
|
return parseQwenConfig(paths)
|
|
464
506
|
case 'pi':
|
|
@@ -476,7 +518,7 @@ export function parseToolConfig(toolMode, paths = getToolConfigPaths()) {
|
|
|
476
518
|
* 📖 Scan all tool configs and return structured results
|
|
477
519
|
*/
|
|
478
520
|
export function scanAllToolConfigs(paths = getToolConfigPaths()) {
|
|
479
|
-
const toolModes = ['goose', 'crush', 'aider', 'qwen', 'pi', 'openhands', 'amp']
|
|
521
|
+
const toolModes = ['goose', 'crush', 'aider', 'kilo', 'qwen', 'pi', 'openhands', 'amp']
|
|
480
522
|
|
|
481
523
|
return toolModes.map((toolMode) => {
|
|
482
524
|
const result = parseToolConfig(toolMode, paths)
|
|
@@ -499,6 +541,7 @@ function getToolEmoji(toolMode) {
|
|
|
499
541
|
goose: '🪿',
|
|
500
542
|
crush: '💘',
|
|
501
543
|
aider: '🛠',
|
|
544
|
+
kilo: '⚡️',
|
|
502
545
|
qwen: '🐉',
|
|
503
546
|
pi: 'π',
|
|
504
547
|
openhands: '🤲',
|
|
@@ -576,6 +619,15 @@ export function softDeleteModel(toolMode, modelId, paths = getToolConfigPaths())
|
|
|
576
619
|
}
|
|
577
620
|
break
|
|
578
621
|
|
|
622
|
+
case 'kilo':
|
|
623
|
+
const kiloConfig = JSON.parse(originalContent)
|
|
624
|
+
if (kiloConfig.model === modelId) {
|
|
625
|
+
delete kiloConfig.model
|
|
626
|
+
newContent = JSON.stringify(kiloConfig, null, 2)
|
|
627
|
+
modified = true
|
|
628
|
+
}
|
|
629
|
+
break
|
|
630
|
+
|
|
579
631
|
case 'qwen':
|
|
580
632
|
const qwenConfig = JSON.parse(originalContent)
|
|
581
633
|
if (qwenConfig.model === modelId) {
|
package/src/key-handler.js
CHANGED
|
@@ -39,7 +39,7 @@ import { getLastLayout, COLUMN_SORT_MAP } from './render-table.js'
|
|
|
39
39
|
import { cycleThemeSetting, detectActiveTheme } from './theme.js'
|
|
40
40
|
import { syncShellEnv, ensureShellRcSource, removeShellEnv } from './shell-env.js'
|
|
41
41
|
import { buildCommandPaletteTree, flattenCommandTree, filterCommandPaletteEntries } from './command-palette.js'
|
|
42
|
-
import { WIDTH_WARNING_MIN_COLS } from './constants.js'
|
|
42
|
+
import { WIDTH_WARNING_MIN_COLS, VERDICT_CYCLE, HEALTH_CYCLE } from './constants.js'
|
|
43
43
|
import { scanAllToolConfigs, softDeleteModel } from './installed-models-manager.js'
|
|
44
44
|
import { startExternalTool } from './tool-launchers.js'
|
|
45
45
|
|
|
@@ -264,6 +264,8 @@ export function createKeyHandler(ctx) {
|
|
|
264
264
|
runUpdate,
|
|
265
265
|
startOpenClaw,
|
|
266
266
|
startOpenCodeDesktop,
|
|
267
|
+
startOpenCodeWeb,
|
|
268
|
+
startKilo,
|
|
267
269
|
startOpenCode,
|
|
268
270
|
startExternalTool,
|
|
269
271
|
getToolModeOrder,
|
|
@@ -495,6 +497,10 @@ export function createKeyHandler(ctx) {
|
|
|
495
497
|
exitCode = await startOpenClaw(userSelected, state.config, { launchCli: true })
|
|
496
498
|
} else if (state.mode === 'opencode-desktop') {
|
|
497
499
|
exitCode = await startOpenCodeDesktop(userSelected, state.config)
|
|
500
|
+
} else if (state.mode === 'opencode-web') {
|
|
501
|
+
exitCode = await startOpenCodeWeb(userSelected, state.config)
|
|
502
|
+
} else if (state.mode === 'kilo') {
|
|
503
|
+
exitCode = await startKilo(userSelected, state.config)
|
|
498
504
|
} else if (state.mode === 'opencode') {
|
|
499
505
|
exitCode = await startOpenCode(userSelected, state.config)
|
|
500
506
|
} else {
|
|
@@ -2414,6 +2420,7 @@ export function createKeyHandler(ctx) {
|
|
|
2414
2420
|
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
2415
2421
|
state.config.settings.footerHidden = state.footerHidden
|
|
2416
2422
|
saveConfig(state.config)
|
|
2423
|
+
state.frame++ // 📖 Force immediate re-render
|
|
2417
2424
|
return
|
|
2418
2425
|
}
|
|
2419
2426
|
|
|
@@ -2449,6 +2456,24 @@ export function createKeyHandler(ctx) {
|
|
|
2449
2456
|
return
|
|
2450
2457
|
}
|
|
2451
2458
|
|
|
2459
|
+
// 📖 Verdict filter key: V = cycle through each verdict (All → Perfect → Normal → Slow → ... → All)
|
|
2460
|
+
if (key.name === 'v') {
|
|
2461
|
+
state.verdictFilterMode = (state.verdictFilterMode + 1) % VERDICT_CYCLE.length
|
|
2462
|
+
applyTierFilter()
|
|
2463
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
2464
|
+
persistUiSettings()
|
|
2465
|
+
return
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// 📖 Health filter key: H = cycle through each health status (All → Up → Timeout → Down → ... → All)
|
|
2469
|
+
if (key.name === 'h') {
|
|
2470
|
+
state.healthFilterMode = (state.healthFilterMode + 1) % HEALTH_CYCLE.length
|
|
2471
|
+
applyTierFilter()
|
|
2472
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
2473
|
+
persistUiSettings()
|
|
2474
|
+
return
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2452
2477
|
// 📖 Help overlay key: Ctrl+H = toggle help overlay
|
|
2453
2478
|
if (key.ctrl && key.name === 'h') {
|
|
2454
2479
|
state.helpVisible = !state.helpVisible
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/kilo-config.js
|
|
3
|
+
* @description Small filesystem helpers for the shared Kilo config file (OpenCode fork).
|
|
4
|
+
*
|
|
5
|
+
* @details
|
|
6
|
+
* 📖 Kilo is a fork of OpenCode and uses the same config structure,
|
|
7
|
+
* 📖 but stored in a different directory: ~/.config/kilo/opencode.json
|
|
8
|
+
*
|
|
9
|
+
* @functions
|
|
10
|
+
* → `loadKiloConfig` — read `~/.config/kilo/opencode.json` safely
|
|
11
|
+
* → `saveKiloConfig` — write `opencode.json` with a simple backup
|
|
12
|
+
*
|
|
13
|
+
* @exports loadKiloConfig, saveKiloConfig
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import { homedir } from 'node:os'
|
|
19
|
+
|
|
20
|
+
const KILO_CONFIG_DIR = join(homedir(), '.config', 'kilo')
|
|
21
|
+
const KILO_CONFIG_PATH = join(KILO_CONFIG_DIR, 'opencode.json')
|
|
22
|
+
const KILO_BACKUP_PATH = join(KILO_CONFIG_DIR, 'opencode.json.bak')
|
|
23
|
+
|
|
24
|
+
export function loadKiloConfig() {
|
|
25
|
+
try {
|
|
26
|
+
if (existsSync(KILO_CONFIG_PATH)) {
|
|
27
|
+
return JSON.parse(readFileSync(KILO_CONFIG_PATH, 'utf8'))
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
return {}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function saveKiloConfig(config) {
|
|
34
|
+
mkdirSync(KILO_CONFIG_DIR, { recursive: true })
|
|
35
|
+
if (existsSync(KILO_CONFIG_PATH)) {
|
|
36
|
+
copyFileSync(KILO_CONFIG_PATH, KILO_BACKUP_PATH)
|
|
37
|
+
}
|
|
38
|
+
writeFileSync(KILO_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getKiloConfigPath() {
|
|
42
|
+
return KILO_CONFIG_PATH
|
|
43
|
+
}
|
package/src/kilo.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file kilo.js
|
|
3
|
+
* @description Kilo CLI integration helpers for direct launches (OpenCode fork).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
import { PROVIDER_COLOR } from './render-table.js'
|
|
8
|
+
import { loadKiloConfig, saveKiloConfig, getKiloConfigPath } from './kilo-config.js'
|
|
9
|
+
import { getApiKey } from './config.js'
|
|
10
|
+
import { ENV_VAR_NAMES, OPENCODE_MODEL_MAP } from './provider-metadata.js'
|
|
11
|
+
import { resolveToolBinaryPath } from './tool-bootstrap.js'
|
|
12
|
+
|
|
13
|
+
// 📖 Map source model IDs to Kilo built-in IDs (same as OpenCode).
|
|
14
|
+
function getKiloModelId(providerKey, modelId) {
|
|
15
|
+
if (providerKey === 'nvidia') return modelId.replace(/^nvidia\//, '')
|
|
16
|
+
if (providerKey === 'zai') return modelId.replace(/^zai\//, '')
|
|
17
|
+
return OPENCODE_MODEL_MAP[providerKey]?.[modelId] || modelId
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 📖 spawnKilo: Resolve API keys + spawn kilo CLI with correct env.
|
|
21
|
+
async function spawnKilo(args, providerKey, fcmConfig) {
|
|
22
|
+
const envVarName = ENV_VAR_NAMES[providerKey]
|
|
23
|
+
const resolvedKey = getApiKey(fcmConfig, providerKey)
|
|
24
|
+
const childEnv = { ...process.env }
|
|
25
|
+
childEnv.NODE_NO_WARNINGS = '1'
|
|
26
|
+
const finalArgs = [...args]
|
|
27
|
+
|
|
28
|
+
if (envVarName && resolvedKey) childEnv[envVarName] = resolvedKey
|
|
29
|
+
|
|
30
|
+
const { spawn } = await import('child_process')
|
|
31
|
+
const child = spawn(resolveToolBinaryPath('kilo') || 'kilo', finalArgs, {
|
|
32
|
+
stdio: 'inherit',
|
|
33
|
+
shell: true,
|
|
34
|
+
detached: false,
|
|
35
|
+
env: childEnv
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
child.on('exit', (code) => resolve(code))
|
|
40
|
+
child.on('error', (err) => {
|
|
41
|
+
if (err.code === 'ENOENT') {
|
|
42
|
+
console.error(chalk.red('\n X Could not find "kilo" -- is it installed and in your PATH?'))
|
|
43
|
+
console.error(chalk.dim(' Install: npm i -g @kilocode/cli or see https://kilo.ai'))
|
|
44
|
+
resolve(1)
|
|
45
|
+
} else {
|
|
46
|
+
reject(err)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Start Kilo CLI ──────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
export async function startKilo(model, fcmConfig) {
|
|
55
|
+
const providerKey = model.providerKey ?? 'nvidia'
|
|
56
|
+
const ocModelId = getKiloModelId(providerKey, model.modelId)
|
|
57
|
+
const modelRef = `${providerKey}/${ocModelId}`
|
|
58
|
+
|
|
59
|
+
console.log(chalk.green(` Setting ${chalk.bold(model.label)} as default...`))
|
|
60
|
+
console.log(chalk.dim(` Model: ${modelRef}`))
|
|
61
|
+
console.log()
|
|
62
|
+
|
|
63
|
+
const config = loadKiloConfig()
|
|
64
|
+
|
|
65
|
+
if (!config.provider) config.provider = {}
|
|
66
|
+
if (!config.provider[providerKey]) {
|
|
67
|
+
// 📖 Auto-configure provider if missing (same as OpenCode logic)
|
|
68
|
+
if (providerKey === 'nvidia') {
|
|
69
|
+
config.provider.nvidia = {
|
|
70
|
+
npm: '@ai-sdk/openai-compatible',
|
|
71
|
+
name: 'NVIDIA NIM',
|
|
72
|
+
options: { baseURL: 'https://integrate.api.nvidia.com/v1', apiKey: '{env:NVIDIA_API_KEY}' },
|
|
73
|
+
models: {}
|
|
74
|
+
}
|
|
75
|
+
} else if (providerKey === 'groq') {
|
|
76
|
+
config.provider.groq = { options: { apiKey: '{env:GROQ_API_KEY}' }, models: {} }
|
|
77
|
+
} else if (providerKey === 'cerebras') {
|
|
78
|
+
config.provider.cerebras = {
|
|
79
|
+
npm: '@ai-sdk/openai-compatible',
|
|
80
|
+
name: 'Cerebras',
|
|
81
|
+
options: { baseURL: 'https://api.cerebras.ai/v1', apiKey: '{env:CEREBRAS_API_KEY}' },
|
|
82
|
+
models: {}
|
|
83
|
+
}
|
|
84
|
+
} else if (providerKey === 'sambanova') {
|
|
85
|
+
config.provider.sambanova = {
|
|
86
|
+
npm: '@ai-sdk/openai-compatible',
|
|
87
|
+
name: 'SambaNova',
|
|
88
|
+
options: { baseURL: 'https://api.sambanova.ai/v1', apiKey: '{env:SAMBANOVA_API_KEY}' },
|
|
89
|
+
models: {}
|
|
90
|
+
}
|
|
91
|
+
} else if (providerKey === 'openrouter') {
|
|
92
|
+
config.provider.openrouter = {
|
|
93
|
+
npm: '@ai-sdk/openai-compatible',
|
|
94
|
+
name: 'OpenRouter',
|
|
95
|
+
options: { baseURL: 'https://openrouter.ai/api/v1', apiKey: '{env:OPENROUTER_API_KEY}' },
|
|
96
|
+
models: {}
|
|
97
|
+
}
|
|
98
|
+
} else if (providerKey === 'huggingface') {
|
|
99
|
+
config.provider.huggingface = {
|
|
100
|
+
npm: '@ai-sdk/openai-compatible',
|
|
101
|
+
name: 'Hugging Face Inference',
|
|
102
|
+
options: { baseURL: 'https://router.huggingface.co/v1', apiKey: '{env:HUGGINGFACE_API_KEY}' },
|
|
103
|
+
models: {}
|
|
104
|
+
}
|
|
105
|
+
} else if (providerKey === 'deepinfra') {
|
|
106
|
+
config.provider.deepinfra = {
|
|
107
|
+
npm: '@ai-sdk/openai-compatible',
|
|
108
|
+
name: 'DeepInfra',
|
|
109
|
+
options: { baseURL: 'https://api.deepinfra.com/v1/openai', apiKey: '{env:DEEPINFRA_API_KEY}' },
|
|
110
|
+
models: {}
|
|
111
|
+
}
|
|
112
|
+
} else if (providerKey === 'fireworks') {
|
|
113
|
+
config.provider.fireworks = {
|
|
114
|
+
npm: '@ai-sdk/openai-compatible',
|
|
115
|
+
name: 'Fireworks AI',
|
|
116
|
+
options: { baseURL: 'https://api.fireworks.ai/inference/v1', apiKey: '{env:FIREWORKS_API_KEY}' },
|
|
117
|
+
models: {}
|
|
118
|
+
}
|
|
119
|
+
} else if (providerKey === 'codestral') {
|
|
120
|
+
config.provider.codestral = {
|
|
121
|
+
npm: '@ai-sdk/openai-compatible',
|
|
122
|
+
name: 'Mistral Codestral',
|
|
123
|
+
options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:CODESTRAL_API_KEY}' },
|
|
124
|
+
models: {}
|
|
125
|
+
}
|
|
126
|
+
} else if (providerKey === 'hyperbolic') {
|
|
127
|
+
config.provider.hyperbolic = {
|
|
128
|
+
npm: '@ai-sdk/openai-compatible',
|
|
129
|
+
name: 'Hyperbolic',
|
|
130
|
+
options: { baseURL: 'https://api.hyperbolic.xyz/v1', apiKey: '{env:HYPERBOLIC_API_KEY}' },
|
|
131
|
+
models: {}
|
|
132
|
+
}
|
|
133
|
+
} else if (providerKey === 'scaleway') {
|
|
134
|
+
config.provider.scaleway = {
|
|
135
|
+
npm: '@ai-sdk/openai-compatible',
|
|
136
|
+
name: 'Scaleway',
|
|
137
|
+
options: { baseURL: 'https://api.scaleway.ai/v1', apiKey: '{env:SCALEWAY_API_KEY}' },
|
|
138
|
+
models: {}
|
|
139
|
+
}
|
|
140
|
+
} else if (providerKey === 'googleai') {
|
|
141
|
+
config.provider.googleai = {
|
|
142
|
+
npm: '@ai-sdk/openai-compatible',
|
|
143
|
+
name: 'Google AI Studio',
|
|
144
|
+
options: { baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: '{env:GOOGLE_API_KEY}' },
|
|
145
|
+
models: {}
|
|
146
|
+
}
|
|
147
|
+
} else if (providerKey === 'siliconflow') {
|
|
148
|
+
config.provider.siliconflow = {
|
|
149
|
+
npm: '@ai-sdk/openai-compatible',
|
|
150
|
+
name: 'SiliconFlow',
|
|
151
|
+
options: { baseURL: 'https://api.siliconflow.com/v1', apiKey: '{env:SILICONFLOW_API_KEY}' },
|
|
152
|
+
models: {}
|
|
153
|
+
}
|
|
154
|
+
} else if (providerKey === 'together') {
|
|
155
|
+
config.provider.together = {
|
|
156
|
+
npm: '@ai-sdk/openai-compatible',
|
|
157
|
+
name: 'Together AI',
|
|
158
|
+
options: { baseURL: 'https://api.together.xyz/v1', apiKey: '{env:TOGETHER_API_KEY}' },
|
|
159
|
+
models: {}
|
|
160
|
+
}
|
|
161
|
+
} else if (providerKey === 'perplexity') {
|
|
162
|
+
config.provider.perplexity = {
|
|
163
|
+
npm: '@ai-sdk/openai-compatible',
|
|
164
|
+
name: 'Perplexity API',
|
|
165
|
+
options: { baseURL: 'https://api.perplexity.ai', apiKey: '{env:PERPLEXITY_API_KEY}' },
|
|
166
|
+
models: {}
|
|
167
|
+
}
|
|
168
|
+
} else if (providerKey === 'chutes') {
|
|
169
|
+
config.provider.chutes = {
|
|
170
|
+
npm: '@ai-sdk/openai-compatible',
|
|
171
|
+
name: 'Chutes AI',
|
|
172
|
+
options: { baseURL: 'https://chutes.ai/v1', apiKey: '{env:CHUTES_API_KEY}' },
|
|
173
|
+
models: {}
|
|
174
|
+
}
|
|
175
|
+
} else if (providerKey === 'ovhcloud') {
|
|
176
|
+
config.provider.ovhcloud = {
|
|
177
|
+
npm: '@ai-sdk/openai-compatible',
|
|
178
|
+
name: 'OVHcloud AI',
|
|
179
|
+
options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
|
|
180
|
+
models: {}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const isBuiltinMapped = OPENCODE_MODEL_MAP[providerKey]?.[model.modelId]
|
|
186
|
+
if (!isBuiltinMapped && config.provider[providerKey]) {
|
|
187
|
+
if (!config.provider[providerKey].models) config.provider[providerKey].models = {}
|
|
188
|
+
config.provider[providerKey].models[ocModelId] = { name: model.label }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
config.model = modelRef
|
|
192
|
+
saveKiloConfig(config)
|
|
193
|
+
|
|
194
|
+
console.log(chalk.dim(` Config saved to: ${getKiloConfigPath()}`))
|
|
195
|
+
console.log()
|
|
196
|
+
console.log(chalk.dim(' Starting Kilo...'))
|
|
197
|
+
console.log()
|
|
198
|
+
|
|
199
|
+
await spawnKilo(['--model', modelRef], providerKey, fcmConfig)
|
|
200
|
+
}
|