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 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.51",
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 both tier and origin filters — model is hidden if it fails either
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
- if (tierHide || originHide) {
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
  }
@@ -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 = 4 // 📖 title, spacer, column headers, separator
97
- export const TABLE_FOOTER_LINES = 5 // 📖 spacer, hints line 1, hints line 2, spacer, credit+links
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) {
@@ -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
+ }