free-coding-models 0.3.17 → 0.3.18
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 +13 -0
- package/README.md +9 -1
- package/package.json +1 -1
- package/src/app.js +18 -2
- package/src/config.js +3 -3
- package/src/key-handler.js +177 -46
- package/src/openclaw.js +39 -5
- package/src/opencode.js +2 -1
- package/src/overlays.js +300 -207
- package/src/render-helpers.js +1 -1
- package/src/render-table.js +140 -177
- package/src/theme.js +291 -43
- package/src/tier-colors.js +15 -17
- package/src/tool-bootstrap.js +310 -0
- package/src/tool-launchers.js +12 -7
- package/src/ui-config.js +24 -31
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## 0.3.18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Missing tool bootstrap flow**: FCM now detects when a target CLI is absent, offers a minimal in-TUI install confirmation, runs the official global install command, then resumes the selected model launch automatically.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **TUI readability overhaul across every screen**: the main table, Settings, Help, Smart Recommend, Feedback, and Changelog overlays now share a semantic high-contrast theme system instead of a patchwork of hardcoded colors.
|
|
12
|
+
- **Global theme switching now works for real**: press `G` to cycle `auto → dark → light` live, and the Settings screen now exposes a visible `Global Theme` row for the same control.
|
|
13
|
+
- **Launcher binary resolution**: direct tool launches now search PATH plus common user bin directories so a freshly installed CLI can be reused immediately in the same FCM session.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Theme repaint bugs**: provider colors, tier colors, separators, badges, cursor highlights, and overlay backgrounds now update immediately when the theme changes instead of keeping stale import-time colors.
|
|
17
|
+
|
|
5
18
|
## 0.3.17
|
|
6
19
|
|
|
7
20
|
### Added
|
package/README.md
CHANGED
|
@@ -103,6 +103,8 @@ free-coding-models
|
|
|
103
103
|
|
|
104
104
|
On first run, you'll be prompted to enter your API key(s). You can skip providers and add more later with **`P`**.
|
|
105
105
|
|
|
106
|
+
Need to fix contrast because your terminal theme is fighting the TUI? Press **`G`** at any time to cycle **Auto → Dark → Light**. The switch recolors the full interface live: table, Settings, Help, Smart Recommend, Feedback, and Changelog.
|
|
107
|
+
|
|
106
108
|
**③ Pick a model and launch your tool:**
|
|
107
109
|
|
|
108
110
|
```
|
|
@@ -111,6 +113,8 @@ On first run, you'll be prompted to enter your API key(s). You can skip provider
|
|
|
111
113
|
|
|
112
114
|
The model you select is automatically written into your tool's config (OpenCode, OpenClaw, Crush, etc.) and the tool opens immediately. Done.
|
|
113
115
|
|
|
116
|
+
If the active CLI tool is missing, FCM now catches it before launch, offers a tiny Yes/No install prompt, installs the tool with its official global command, then resumes the same model launch automatically.
|
|
117
|
+
|
|
114
118
|
> 💡 You can also run `free-coding-models --goose --tier S` to pre-filter to S-tier models for Goose before the TUI even opens.
|
|
115
119
|
|
|
116
120
|
|
|
@@ -171,8 +175,9 @@ Press **`Z`** in the TUI to cycle between tools without restarting.
|
|
|
171
175
|
| `D` | Cycle provider filter |
|
|
172
176
|
| `E` | Toggle configured-only mode |
|
|
173
177
|
| `F` | Favorite / unfavorite model |
|
|
178
|
+
| `G` | Cycle global theme (`Auto → Dark → Light`) |
|
|
174
179
|
| `R/S/C/M/O/L/A/H/V/B/U` | Sort columns |
|
|
175
|
-
| `P` | Settings (API keys, providers, updates) |
|
|
180
|
+
| `P` | Settings (API keys, providers, updates, theme) |
|
|
176
181
|
| `Y` | Install Endpoints (push provider into tool config) |
|
|
177
182
|
| `Q` | Smart Recommend overlay |
|
|
178
183
|
| `N` | Changelog |
|
|
@@ -196,7 +201,10 @@ Press **`Z`** in the TUI to cycle between tools without restarting.
|
|
|
196
201
|
- **Keyless latency** — models ping even without an API key (show 🔑 NO KEY)
|
|
197
202
|
- **Smart Recommend** — questionnaire picks the best model for your task type
|
|
198
203
|
- **Install Endpoints** — push a full provider catalog into any tool's config (`Y`)
|
|
204
|
+
- **Missing tool bootstrap** — detect absent CLIs, offer one-click install, then continue the selected launch automatically
|
|
199
205
|
- **Width guardrail** — shows a warning instead of a broken table in narrow terminals
|
|
206
|
+
- **Readable everywhere** — semantic theme palette keeps table rows, overlays, badges, and help screens legible in dark and light terminals
|
|
207
|
+
- **Global theme switch** — `G` cycles `auto`, `dark`, and `light` live without restarting
|
|
200
208
|
- **Auto-retry** — timeout models keep getting retried
|
|
201
209
|
|
|
202
210
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
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
|
@@ -122,6 +122,7 @@ import { createOverlayRenderers } from '../src/overlays.js'
|
|
|
122
122
|
import { createKeyHandler } from '../src/key-handler.js'
|
|
123
123
|
import { getToolModeOrder, getToolMeta } from '../src/tool-metadata.js'
|
|
124
124
|
import { startExternalTool } from '../src/tool-launchers.js'
|
|
125
|
+
import { getToolInstallPlan, installToolWithPlan, isToolInstalled } from '../src/tool-bootstrap.js'
|
|
125
126
|
import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels } from '../src/endpoint-installer.js'
|
|
126
127
|
import { loadCache, saveCache, clearCache, getCacheAge } from '../src/cache.js'
|
|
127
128
|
import { checkConfigSecurity } from '../src/security.js'
|
|
@@ -176,7 +177,7 @@ const LOCAL_VERSION = pkg.version
|
|
|
176
177
|
export async function runApp(cliArgs, config) {
|
|
177
178
|
|
|
178
179
|
// 📖 Detect user active terminal theme
|
|
179
|
-
detectActiveTheme(config.settings?.theme || '
|
|
180
|
+
detectActiveTheme(config.settings?.theme || 'auto')
|
|
180
181
|
|
|
181
182
|
// 📖 Check config file security — warn and offer auto-fix if permissions are too open
|
|
182
183
|
const securityCheck = checkConfigSecurity()
|
|
@@ -420,6 +421,14 @@ export async function runApp(cliArgs, config) {
|
|
|
420
421
|
installEndpointsSelectedModelIds: new Set(), // 📖 Multi-select buffer for the selected-models phase
|
|
421
422
|
installEndpointsErrorMsg: null, // 📖 Temporary validation/error message inside the install flow
|
|
422
423
|
installEndpointsResult: null, // 📖 Final install result shown in the result phase
|
|
424
|
+
// 📖 Missing-tool bootstrap overlay — confirms a one-click install before retrying the launch.
|
|
425
|
+
toolInstallPromptOpen: false,
|
|
426
|
+
toolInstallPromptCursor: 0,
|
|
427
|
+
toolInstallPromptScrollOffset: 0,
|
|
428
|
+
toolInstallPromptMode: null,
|
|
429
|
+
toolInstallPromptModel: null,
|
|
430
|
+
toolInstallPromptPlan: null,
|
|
431
|
+
toolInstallPromptErrorMsg: null,
|
|
423
432
|
// 📖 Smart Recommend overlay state (Q key opens it)
|
|
424
433
|
recommendOpen: false, // 📖 Whether the recommend overlay is active
|
|
425
434
|
recommendPhase: 'questionnaire', // 📖 'questionnaire'|'analyzing'|'results' — current phase
|
|
@@ -750,6 +759,8 @@ export async function runApp(cliArgs, config) {
|
|
|
750
759
|
getInstallTargetModes,
|
|
751
760
|
getProviderCatalogModels,
|
|
752
761
|
getToolMeta,
|
|
762
|
+
getToolInstallPlan,
|
|
763
|
+
padEndDisplay,
|
|
753
764
|
})
|
|
754
765
|
|
|
755
766
|
onKeyPress = createKeyHandler({
|
|
@@ -785,6 +796,9 @@ export async function runApp(cliArgs, config) {
|
|
|
785
796
|
startOpenCode,
|
|
786
797
|
startExternalTool,
|
|
787
798
|
getToolModeOrder,
|
|
799
|
+
getToolInstallPlan,
|
|
800
|
+
isToolInstalled,
|
|
801
|
+
installToolWithPlan,
|
|
788
802
|
startRecommendAnalysis: overlays.startRecommendAnalysis,
|
|
789
803
|
stopRecommendAnalysis: overlays.stopRecommendAnalysis,
|
|
790
804
|
sendBugReport,
|
|
@@ -844,7 +858,7 @@ export async function runApp(cliArgs, config) {
|
|
|
844
858
|
refreshAutoPingMode()
|
|
845
859
|
state.frame++
|
|
846
860
|
// 📖 Cache visible+sorted models each frame so Enter handler always matches the display
|
|
847
|
-
if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen) {
|
|
861
|
+
if (!state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen) {
|
|
848
862
|
const visible = state.results.filter(r => !r.hidden)
|
|
849
863
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
850
864
|
}
|
|
@@ -852,6 +866,8 @@ export async function runApp(cliArgs, config) {
|
|
|
852
866
|
? overlays.renderSettings()
|
|
853
867
|
: state.installEndpointsOpen
|
|
854
868
|
? overlays.renderInstallEndpoints()
|
|
869
|
+
: state.toolInstallPromptOpen
|
|
870
|
+
? overlays.renderToolInstallPrompt()
|
|
855
871
|
: state.recommendOpen
|
|
856
872
|
? overlays.renderRecommend()
|
|
857
873
|
: state.feedbackOpen
|
package/src/config.js
CHANGED
|
@@ -210,7 +210,7 @@ function normalizeSettingsSection(settings) {
|
|
|
210
210
|
...safeSettings,
|
|
211
211
|
hideUnconfiguredModels: typeof safeSettings.hideUnconfiguredModels === 'boolean' ? safeSettings.hideUnconfiguredModels : true,
|
|
212
212
|
disableWidthsWarning: safeSettings.disableWidthsWarning === true,
|
|
213
|
-
theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : '
|
|
213
|
+
theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : 'auto',
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
|
|
@@ -231,7 +231,7 @@ function normalizeProfileSettings(settings) {
|
|
|
231
231
|
..._emptyProfileSettings(),
|
|
232
232
|
...safeSettings,
|
|
233
233
|
disableWidthsWarning: safeSettings.disableWidthsWarning === true,
|
|
234
|
-
theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : '
|
|
234
|
+
theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : 'auto',
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
@@ -844,7 +844,7 @@ export function _emptyProfileSettings() {
|
|
|
844
844
|
hideUnconfiguredModels: true, // 📖 true = default to providers that are actually configured
|
|
845
845
|
preferredToolMode: 'opencode', // 📖 remember the last Z-selected launcher across app restarts
|
|
846
846
|
disableWidthsWarning: false, // 📖 Disable widths warning (default off)
|
|
847
|
-
theme: '
|
|
847
|
+
theme: 'auto', // 📖 'auto' follows the terminal/OS theme, override with 'dark' or 'light' if needed
|
|
848
848
|
}
|
|
849
849
|
}
|
|
850
850
|
|
package/src/key-handler.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { loadChangelog } from './changelog-loader.js'
|
|
31
31
|
import { loadConfig, replaceConfigContents } from './config.js'
|
|
32
32
|
import { cleanupLegacyProxyArtifacts } from './legacy-proxy-cleanup.js'
|
|
33
|
+
import { cycleThemeSetting, detectActiveTheme } from './theme.js'
|
|
33
34
|
|
|
34
35
|
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
35
36
|
// 📖 is not guaranteed to be accepted by their chat endpoint.
|
|
@@ -184,6 +185,9 @@ export function createKeyHandler(ctx) {
|
|
|
184
185
|
startOpenCode,
|
|
185
186
|
startExternalTool,
|
|
186
187
|
getToolModeOrder,
|
|
188
|
+
getToolInstallPlan,
|
|
189
|
+
isToolInstalled,
|
|
190
|
+
installToolWithPlan,
|
|
187
191
|
startRecommendAnalysis,
|
|
188
192
|
stopRecommendAnalysis,
|
|
189
193
|
sendBugReport,
|
|
@@ -207,6 +211,97 @@ export function createKeyHandler(ctx) {
|
|
|
207
211
|
|
|
208
212
|
let userSelected = null
|
|
209
213
|
|
|
214
|
+
function resetToolInstallPrompt() {
|
|
215
|
+
state.toolInstallPromptOpen = false
|
|
216
|
+
state.toolInstallPromptCursor = 0
|
|
217
|
+
state.toolInstallPromptScrollOffset = 0
|
|
218
|
+
state.toolInstallPromptMode = null
|
|
219
|
+
state.toolInstallPromptModel = null
|
|
220
|
+
state.toolInstallPromptPlan = null
|
|
221
|
+
state.toolInstallPromptErrorMsg = null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function shouldCheckMissingTool(mode) {
|
|
225
|
+
return mode !== 'opencode-desktop'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function launchSelectedModel(selected, options = {}) {
|
|
229
|
+
const { uiAlreadyStopped = false } = options
|
|
230
|
+
userSelected = { modelId: selected.modelId, label: selected.label, tier: selected.tier, providerKey: selected.providerKey }
|
|
231
|
+
|
|
232
|
+
if (!uiAlreadyStopped) {
|
|
233
|
+
readline.emitKeypressEvents(process.stdin)
|
|
234
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true)
|
|
235
|
+
stopUi()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 📖 Show selection status before handing control to the target tool.
|
|
239
|
+
if (selected.status === 'timeout') {
|
|
240
|
+
console.log(chalk.yellow(` ⚠ Selected: ${selected.label} (currently timing out)`))
|
|
241
|
+
} else if (selected.status === 'down') {
|
|
242
|
+
console.log(chalk.red(` ⚠ Selected: ${selected.label} (currently down)`))
|
|
243
|
+
} else {
|
|
244
|
+
console.log(chalk.cyan(` ✓ Selected: ${selected.label}`))
|
|
245
|
+
}
|
|
246
|
+
console.log()
|
|
247
|
+
|
|
248
|
+
// 📖 OpenClaw manages API keys inside its own config file. All other tools
|
|
249
|
+
// 📖 still need a provider key to be useful, so keep the existing warning.
|
|
250
|
+
if (state.mode !== 'openclaw') {
|
|
251
|
+
const selectedApiKey = getApiKey(state.config, selected.providerKey)
|
|
252
|
+
if (!selectedApiKey) {
|
|
253
|
+
console.log(chalk.yellow(` Warning: No API key configured for ${selected.providerKey}.`))
|
|
254
|
+
console.log(chalk.yellow(` The selected tool may not be able to use ${selected.label}.`))
|
|
255
|
+
console.log(chalk.dim(` Set ${ENV_VAR_NAMES[selected.providerKey] || selected.providerKey.toUpperCase() + '_API_KEY'} or configure via settings (P key).`))
|
|
256
|
+
console.log()
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let exitCode = 0
|
|
261
|
+
if (state.mode === 'openclaw') {
|
|
262
|
+
exitCode = await startOpenClaw(userSelected, state.config, { launchCli: true })
|
|
263
|
+
} else if (state.mode === 'opencode-desktop') {
|
|
264
|
+
exitCode = await startOpenCodeDesktop(userSelected, state.config)
|
|
265
|
+
} else if (state.mode === 'opencode') {
|
|
266
|
+
exitCode = await startOpenCode(userSelected, state.config)
|
|
267
|
+
} else {
|
|
268
|
+
exitCode = await startExternalTool(state.mode, userSelected, state.config)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
process.exit(typeof exitCode === 'number' ? exitCode : 0)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function installMissingToolAndLaunch(selected, installPlan) {
|
|
275
|
+
const currentPlan = installPlan || getToolInstallPlan(state.mode)
|
|
276
|
+
stopUi({ resetRawMode: true })
|
|
277
|
+
|
|
278
|
+
console.log(chalk.cyan(` 📦 Installing missing tool for ${state.mode}...`))
|
|
279
|
+
if (currentPlan?.summary) console.log(chalk.dim(` ${currentPlan.summary}`))
|
|
280
|
+
if (currentPlan?.shellCommand) console.log(chalk.dim(` ${currentPlan.shellCommand}`))
|
|
281
|
+
if (currentPlan?.note) console.log(chalk.dim(` ${currentPlan.note}`))
|
|
282
|
+
console.log()
|
|
283
|
+
|
|
284
|
+
const installResult = await installToolWithPlan(currentPlan)
|
|
285
|
+
if (!installResult.ok) {
|
|
286
|
+
console.log(chalk.red(` X Tool installation failed with exit code ${installResult.exitCode}.`))
|
|
287
|
+
if (currentPlan?.docsUrl) console.log(chalk.dim(` Docs: ${currentPlan.docsUrl}`))
|
|
288
|
+
console.log()
|
|
289
|
+
process.exit(installResult.exitCode || 1)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (shouldCheckMissingTool(state.mode) && !isToolInstalled(state.mode)) {
|
|
293
|
+
console.log(chalk.yellow(' ⚠ The installer finished, but the tool is still not reachable from this terminal session.'))
|
|
294
|
+
console.log(chalk.dim(' Restart your shell or add the tool bin directory to PATH, then retry the launch.'))
|
|
295
|
+
if (currentPlan?.docsUrl) console.log(chalk.dim(` Docs: ${currentPlan.docsUrl}`))
|
|
296
|
+
console.log()
|
|
297
|
+
process.exit(1)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.log(chalk.green(' ✓ Tool installed successfully. Continuing with the selected model...'))
|
|
301
|
+
console.log()
|
|
302
|
+
await launchSelectedModel(selected, { uiAlreadyStopped: true })
|
|
303
|
+
}
|
|
304
|
+
|
|
210
305
|
// ─── Settings key test helper ───────────────────────────────────────────────
|
|
211
306
|
// 📖 Fires a single ping to the selected provider to verify the API key works.
|
|
212
307
|
async function testProviderKey(providerKey) {
|
|
@@ -383,6 +478,20 @@ export function createKeyHandler(ctx) {
|
|
|
383
478
|
saveConfig(state.config)
|
|
384
479
|
}
|
|
385
480
|
|
|
481
|
+
// 📖 Theme switches need to update both persisted preference and the live
|
|
482
|
+
// 📖 semantic palette immediately so every screen redraw adopts the new colors.
|
|
483
|
+
function applyThemeSetting(nextTheme) {
|
|
484
|
+
if (!state.config.settings) state.config.settings = {}
|
|
485
|
+
state.config.settings.theme = nextTheme
|
|
486
|
+
saveConfig(state.config)
|
|
487
|
+
detectActiveTheme(nextTheme)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function cycleGlobalTheme() {
|
|
491
|
+
const currentTheme = state.config.settings?.theme || 'auto'
|
|
492
|
+
applyThemeSetting(cycleThemeSetting(currentTheme))
|
|
493
|
+
}
|
|
494
|
+
|
|
386
495
|
function resetInstallEndpointsOverlay() {
|
|
387
496
|
state.installEndpointsOpen = false
|
|
388
497
|
state.installEndpointsPhase = 'providers'
|
|
@@ -431,6 +540,11 @@ export function createKeyHandler(ctx) {
|
|
|
431
540
|
if (!key) return
|
|
432
541
|
noteUserActivity()
|
|
433
542
|
|
|
543
|
+
if (!state.feedbackOpen && !state.settingsEditMode && !state.settingsAddKeyMode && key.name === 'g' && !key.ctrl && !key.meta) {
|
|
544
|
+
cycleGlobalTheme()
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
|
|
434
548
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
435
549
|
|
|
436
550
|
// 📖 Install Endpoints overlay: provider → tool → connection → scope → optional model subset.
|
|
@@ -614,6 +728,44 @@ export function createKeyHandler(ctx) {
|
|
|
614
728
|
return
|
|
615
729
|
}
|
|
616
730
|
|
|
731
|
+
if (state.toolInstallPromptOpen) {
|
|
732
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
733
|
+
|
|
734
|
+
const installPlan = state.toolInstallPromptPlan || getToolInstallPlan(state.toolInstallPromptMode)
|
|
735
|
+
const installSupported = Boolean(installPlan?.supported)
|
|
736
|
+
|
|
737
|
+
if (key.name === 'escape') {
|
|
738
|
+
resetToolInstallPrompt()
|
|
739
|
+
return
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (installSupported && key.name === 'up') {
|
|
743
|
+
state.toolInstallPromptCursor = Math.max(0, state.toolInstallPromptCursor - 1)
|
|
744
|
+
return
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (installSupported && key.name === 'down') {
|
|
748
|
+
state.toolInstallPromptCursor = Math.min(1, state.toolInstallPromptCursor + 1)
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (key.name === 'return') {
|
|
753
|
+
if (!installSupported) {
|
|
754
|
+
resetToolInstallPrompt()
|
|
755
|
+
return
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const selectedModel = state.toolInstallPromptModel
|
|
759
|
+
const shouldInstall = state.toolInstallPromptCursor === 0
|
|
760
|
+
resetToolInstallPrompt()
|
|
761
|
+
|
|
762
|
+
if (!shouldInstall || !selectedModel) return
|
|
763
|
+
await installMissingToolAndLaunch(selectedModel, installPlan)
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return
|
|
767
|
+
}
|
|
768
|
+
|
|
617
769
|
// 📖 Feedback overlay: intercept ALL keys while overlay is active.
|
|
618
770
|
// 📖 Enter → send to Discord, Esc → cancel, Backspace → delete char, printable → append to buffer.
|
|
619
771
|
if (state.feedbackOpen) {
|
|
@@ -1078,6 +1230,11 @@ export function createKeyHandler(ctx) {
|
|
|
1078
1230
|
return
|
|
1079
1231
|
}
|
|
1080
1232
|
|
|
1233
|
+
if (state.settingsCursor === themeRowIdx) {
|
|
1234
|
+
cycleGlobalTheme()
|
|
1235
|
+
return
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1081
1238
|
if (state.settingsCursor === cleanupLegacyProxyRowIdx) {
|
|
1082
1239
|
runLegacyProxyCleanup()
|
|
1083
1240
|
return
|
|
@@ -1098,6 +1255,7 @@ export function createKeyHandler(ctx) {
|
|
|
1098
1255
|
|
|
1099
1256
|
// 📖 Enter edit mode for the selected provider's key
|
|
1100
1257
|
const pk = providerKeys[state.settingsCursor]
|
|
1258
|
+
if (!pk) return
|
|
1101
1259
|
state.settingsEditBuffer = resolveApiKeys(state.config, pk)[0] ?? ''
|
|
1102
1260
|
state.settingsEditMode = true
|
|
1103
1261
|
return
|
|
@@ -1112,15 +1270,7 @@ export function createKeyHandler(ctx) {
|
|
|
1112
1270
|
) return
|
|
1113
1271
|
// 📖 Theme configuration cycle inside settings
|
|
1114
1272
|
if (state.settingsCursor === themeRowIdx) {
|
|
1115
|
-
|
|
1116
|
-
const currentTheme = state.config.settings?.theme || 'dark'
|
|
1117
|
-
const nextIndex = (themes.indexOf(currentTheme) + 1) % themes.length
|
|
1118
|
-
state.config.settings.theme = themes[nextIndex]
|
|
1119
|
-
saveConfig(state.config)
|
|
1120
|
-
try {
|
|
1121
|
-
const { detectActiveTheme } = await import('../src/theme.js')
|
|
1122
|
-
detectActiveTheme(state.config.settings.theme)
|
|
1123
|
-
} catch {}
|
|
1273
|
+
cycleGlobalTheme()
|
|
1124
1274
|
return
|
|
1125
1275
|
}
|
|
1126
1276
|
// 📖 Widths Warning toggle (disable/enable)
|
|
@@ -1142,6 +1292,8 @@ export function createKeyHandler(ctx) {
|
|
|
1142
1292
|
if (key.name === 't') {
|
|
1143
1293
|
if (
|
|
1144
1294
|
state.settingsCursor === updateRowIdx
|
|
1295
|
+
|| state.settingsCursor === widthWarningRowIdx
|
|
1296
|
+
|| state.settingsCursor === themeRowIdx
|
|
1145
1297
|
|| state.settingsCursor === cleanupLegacyProxyRowIdx
|
|
1146
1298
|
|| state.settingsCursor === changelogViewRowIdx
|
|
1147
1299
|
) return
|
|
@@ -1149,6 +1301,7 @@ export function createKeyHandler(ctx) {
|
|
|
1149
1301
|
|
|
1150
1302
|
// 📖 Test the selected provider's key (fires a real ping)
|
|
1151
1303
|
const pk = providerKeys[state.settingsCursor]
|
|
1304
|
+
if (!pk) return
|
|
1152
1305
|
testProviderKey(pk)
|
|
1153
1306
|
return
|
|
1154
1307
|
}
|
|
@@ -1442,46 +1595,24 @@ export function createKeyHandler(ctx) {
|
|
|
1442
1595
|
// 📖 Use the cached visible+sorted array — guaranteed to match what's on screen
|
|
1443
1596
|
const selected = state.visibleSorted[state.cursor]
|
|
1444
1597
|
if (!selected) return // 📖 Guard: empty visible list (all filtered out)
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
} else if (selected.status === 'down') {
|
|
1457
|
-
console.log(chalk.red(` ⚠ Selected: ${selected.label} (currently down)`))
|
|
1458
|
-
} else {
|
|
1459
|
-
console.log(chalk.cyan(` ✓ Selected: ${selected.label}`))
|
|
1460
|
-
}
|
|
1461
|
-
console.log()
|
|
1462
|
-
|
|
1463
|
-
// 📖 Warn if no API key is configured for the selected model's provider
|
|
1464
|
-
if (state.mode !== 'openclaw') {
|
|
1465
|
-
const selectedApiKey = getApiKey(state.config, selected.providerKey)
|
|
1466
|
-
if (!selectedApiKey) {
|
|
1467
|
-
console.log(chalk.yellow(` Warning: No API key configured for ${selected.providerKey}.`))
|
|
1468
|
-
console.log(chalk.yellow(` The selected tool may not be able to use ${selected.label}.`))
|
|
1469
|
-
console.log(chalk.dim(` Set ${ENV_VAR_NAMES[selected.providerKey] || selected.providerKey.toUpperCase() + '_API_KEY'} or configure via settings (P key).`))
|
|
1470
|
-
console.log()
|
|
1598
|
+
if (shouldCheckMissingTool(state.mode) && !isToolInstalled(state.mode)) {
|
|
1599
|
+
state.toolInstallPromptOpen = true
|
|
1600
|
+
state.toolInstallPromptCursor = 0
|
|
1601
|
+
state.toolInstallPromptScrollOffset = 0
|
|
1602
|
+
state.toolInstallPromptMode = state.mode
|
|
1603
|
+
state.toolInstallPromptModel = {
|
|
1604
|
+
modelId: selected.modelId,
|
|
1605
|
+
label: selected.label,
|
|
1606
|
+
tier: selected.tier,
|
|
1607
|
+
providerKey: selected.providerKey,
|
|
1608
|
+
status: selected.status,
|
|
1471
1609
|
}
|
|
1610
|
+
state.toolInstallPromptPlan = getToolInstallPlan(state.mode)
|
|
1611
|
+
state.toolInstallPromptErrorMsg = null
|
|
1612
|
+
return
|
|
1472
1613
|
}
|
|
1473
1614
|
|
|
1474
|
-
|
|
1475
|
-
if (state.mode === 'openclaw') {
|
|
1476
|
-
await startOpenClaw(userSelected, state.config)
|
|
1477
|
-
} else if (state.mode === 'opencode-desktop') {
|
|
1478
|
-
await startOpenCodeDesktop(userSelected, state.config)
|
|
1479
|
-
} else if (state.mode === 'opencode') {
|
|
1480
|
-
await startOpenCode(userSelected, state.config)
|
|
1481
|
-
} else {
|
|
1482
|
-
await startExternalTool(state.mode, userSelected, state.config)
|
|
1483
|
-
}
|
|
1484
|
-
process.exit(0)
|
|
1615
|
+
await launchSelectedModel(selected)
|
|
1485
1616
|
}
|
|
1486
1617
|
}
|
|
1487
1618
|
}
|
package/src/openclaw.js
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* @description OpenClaw config helpers for persisting the selected provider/model as the default.
|
|
4
4
|
*
|
|
5
5
|
* @details
|
|
6
|
-
* 📖 OpenClaw is config-driven
|
|
6
|
+
* 📖 OpenClaw is primarily config-driven, but FCM can now optionally launch the
|
|
7
|
+
* 📖 installed CLI right after persisting the selected default model.
|
|
7
8
|
* 📖 Pressing Enter in `OpenClaw` mode must therefore do two things reliably:
|
|
8
9
|
* - install the selected provider/model into `~/.openclaw/openclaw.json`
|
|
9
10
|
* - set that exact model as the default primary model for the next OpenClaw session
|
|
11
|
+
* - optionally start `openclaw` immediately when the caller asks for it
|
|
10
12
|
*
|
|
11
13
|
* 📖 The old implementation was hard-coded to `nvidia/*`, which meant selecting
|
|
12
14
|
* 📖 a Groq/Cerebras/etc. row silently wrote the wrong provider/model into the
|
|
@@ -30,6 +32,7 @@ import { dirname, join } from 'path'
|
|
|
30
32
|
import { installProviderEndpoints } from './endpoint-installer.js'
|
|
31
33
|
import { ENV_VAR_NAMES } from './provider-metadata.js'
|
|
32
34
|
import { PROVIDER_COLOR } from './render-table.js'
|
|
35
|
+
import { resolveToolBinaryPath } from './tool-bootstrap.js'
|
|
33
36
|
|
|
34
37
|
const OPENCLAW_CONFIG = join(homedir(), '.openclaw', 'openclaw.json')
|
|
35
38
|
|
|
@@ -53,13 +56,38 @@ export function saveOpenClawConfig(config, options = {}) {
|
|
|
53
56
|
writeFileSync(filePath, JSON.stringify(config, null, 2))
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
function spawnOpenClawCli() {
|
|
60
|
+
return new Promise(async (resolve, reject) => {
|
|
61
|
+
const { spawn } = await import('child_process')
|
|
62
|
+
const command = resolveToolBinaryPath('openclaw') || 'openclaw'
|
|
63
|
+
const child = spawn(command, [], {
|
|
64
|
+
stdio: 'inherit',
|
|
65
|
+
shell: false,
|
|
66
|
+
detached: false,
|
|
67
|
+
env: process.env,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
child.on('exit', (code) => resolve(typeof code === 'number' ? code : 0))
|
|
71
|
+
child.on('error', (error) => {
|
|
72
|
+
if (error?.code === 'ENOENT') {
|
|
73
|
+
console.log(chalk.red(' X Could not find "openclaw" in PATH.'))
|
|
74
|
+
console.log(chalk.dim(' Install: npm install -g openclaw@latest or see https://docs.openclaw.ai/install'))
|
|
75
|
+
console.log()
|
|
76
|
+
resolve(1)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
reject(error)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
/**
|
|
57
85
|
* 📖 startOpenClaw installs the selected provider/model into OpenClaw and sets
|
|
58
86
|
* 📖 it as the primary default model. OpenClaw itself is not launched here.
|
|
59
87
|
*
|
|
60
88
|
* @param {{ providerKey: string, modelId: string, label: string }} model
|
|
61
89
|
* @param {Record<string, unknown>} config
|
|
62
|
-
* @param {{ paths?: { openclawConfigPath?: string } }} [options]
|
|
90
|
+
* @param {{ paths?: { openclawConfigPath?: string }, launchCli?: boolean }} [options]
|
|
63
91
|
* @returns {Promise<ReturnType<typeof installProviderEndpoints> | null>}
|
|
64
92
|
*/
|
|
65
93
|
export async function startOpenClaw(model, config, options = {}) {
|
|
@@ -85,9 +113,15 @@ export async function startOpenClaw(model, config, options = {}) {
|
|
|
85
113
|
if (result.backupPath) console.log(chalk.dim(` 💾 Backup: ${result.backupPath}`))
|
|
86
114
|
if (providerEnvName) console.log(chalk.dim(` 🔑 API key synced under config env.${providerEnvName}`))
|
|
87
115
|
console.log()
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
if (options.launchCli) {
|
|
117
|
+
console.log(chalk.dim(' Starting OpenClaw...'))
|
|
118
|
+
console.log()
|
|
119
|
+
await spawnOpenClawCli()
|
|
120
|
+
} else {
|
|
121
|
+
console.log(chalk.dim(' 💡 OpenClaw will reload config automatically when it notices the file change.'))
|
|
122
|
+
console.log(chalk.dim(` To apply manually: openclaw models set ${result.primaryModelRef || `${result.providerId}/${model.modelId}`}`))
|
|
123
|
+
console.log()
|
|
124
|
+
}
|
|
91
125
|
return result
|
|
92
126
|
} catch (error) {
|
|
93
127
|
console.log(chalk.red(` X Could not configure OpenClaw: ${error instanceof Error ? error.message : String(error)}`))
|
package/src/opencode.js
CHANGED
|
@@ -32,6 +32,7 @@ import { PROVIDER_COLOR } from './render-table.js'
|
|
|
32
32
|
import { loadOpenCodeConfig, saveOpenCodeConfig } from './opencode-config.js'
|
|
33
33
|
import { getApiKey } from './config.js'
|
|
34
34
|
import { ENV_VAR_NAMES, OPENCODE_MODEL_MAP, isWindows, isMac, isLinux } from './provider-metadata.js'
|
|
35
|
+
import { resolveToolBinaryPath } from './tool-bootstrap.js'
|
|
35
36
|
|
|
36
37
|
// 📖 OpenCode config location: ~/.config/opencode/opencode.json on ALL platforms.
|
|
37
38
|
// 📖 OpenCode uses xdg-basedir which resolves to %USERPROFILE%\.config on Windows.
|
|
@@ -177,7 +178,7 @@ async function spawnOpenCode(args, providerKey, fcmConfig, existingZaiProxy = nu
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
const { spawn } = await import('child_process')
|
|
180
|
-
const child = spawn('opencode', finalArgs, {
|
|
181
|
+
const child = spawn(resolveToolBinaryPath('opencode') || 'opencode', finalArgs, {
|
|
181
182
|
stdio: 'inherit',
|
|
182
183
|
shell: true,
|
|
183
184
|
detached: false,
|