free-coding-models 0.2.1 β†’ 0.2.3

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/README.md CHANGED
@@ -84,6 +84,7 @@
84
84
  - **πŸ’» OpenCode integration** β€” Auto-detects NIM setup, sets model as default, launches OpenCode
85
85
  - **🦞 OpenClaw integration** β€” Sets selected model as default provider in `~/.openclaw/openclaw.json`
86
86
  - **🧰 Public tool launchers** β€” `Enter` can auto-configure and launch `OpenCode CLI`, `OpenCode Desktop`, `OpenClaw`, `Crush`, and `Goose`
87
+ - **πŸ”Œ Install Endpoints flow** β€” Press `Y` to install one configured provider directly into `OpenCode CLI`, `OpenCode Desktop`, `OpenClaw`, `Crush`, or `Goose`, either with the full provider catalog or a curated subset of models
87
88
  - **πŸ“ Feature Request (J key)** β€” Send anonymous feedback directly to the project team
88
89
  - **πŸ› Bug Report (I key)** β€” Send anonymous bug reports directly to the project team
89
90
  - **🎨 Clean output** β€” Zero scrollback pollution, interface stays open until Ctrl+C
@@ -446,7 +447,7 @@ The main table displays one row per model with the following columns:
446
447
  | Column | Sort key | Description |
447
448
  |--------|----------|-------------|
448
449
  | **Rank** | `R` | Position based on current sort order (medals for top 3: πŸ₯‡πŸ₯ˆπŸ₯‰) |
449
- | **Tier** | `Y` | SWE-bench tier (S+, S, A+, A, A-, B+, B, C) |
450
+ | **Tier** | β€” | SWE-bench tier (S+, S, A+, A, A-, B+, B, C) |
450
451
  | **SWE%** | `S` | SWE-bench Verified score β€” industry-standard for coding |
451
452
  | **CTX** | `C` | Context window size (e.g. `128k`) |
452
453
  | **Model** | `M` | Model display name (favorites show ⭐ prefix) |
@@ -844,7 +845,7 @@ This script:
844
845
  **Keyboard shortcuts (main TUI):**
845
846
  - **↑↓** β€” Navigate models
846
847
  - **Enter** β€” Select model and launch the current target tool from the header badge
847
- - **R/Y/S/C/M/O/L/A/H/V/B/U/G** β€” Sort by Rank/Tier/SWE/Ctx/Model/Provider/Latest/Avg/Health/Verdict/Stability/Up%/Usage
848
+ - **R/S/C/M/O/L/A/H/V/B/U/G** β€” Sort by Rank/SWE/Ctx/Model/Provider/Latest/Avg/Health/Verdict/Stability/Up%/Usage
848
849
  - **F** β€” Toggle favorite on selected model (⭐ in Model column, pinned at top)
849
850
  - **T** β€” Cycle tier filter (All β†’ S+ β†’ S β†’ A+ β†’ A β†’ A- β†’ B+ β†’ B β†’ C β†’ All)
850
851
  - **D** β€” Cycle provider filter (All β†’ NIM β†’ Groq β†’ ...)
@@ -852,6 +853,7 @@ This script:
852
853
  - **Z** β€” Cycle target tool (OpenCode CLI β†’ OpenCode Desktop β†’ OpenClaw β†’ Crush β†’ Goose)
853
854
  - **X** β€” Toggle request logs (recent proxied request/token usage logs)
854
855
  - **P** β€” Open Settings (manage API keys, toggles, updates, profiles)
856
+ - **Y** β€” Open Install Endpoints (`provider β†’ tool β†’ all models` or `selected models only`, no proxy)
855
857
  - **Shift+P** β€” Cycle through saved profiles (switches live TUI settings)
856
858
  - **Shift+S** β€” Save current TUI settings as a named profile (inline prompt)
857
859
  - **Q** β€” Open Smart Recommend overlay (find the best model for your task)
@@ -862,6 +864,21 @@ This script:
862
864
 
863
865
  Pressing **K** now shows a full in-app reference: main hotkeys, settings hotkeys, and CLI flags with usage examples.
864
866
 
867
+ ### πŸ”Œ Install Endpoints (`Y`)
868
+
869
+ `Y` opens a dedicated install flow for configured providers. The flow is:
870
+
871
+ 1. Pick one provider that already has an API key in Settings
872
+ 2. Pick the target tool: `OpenCode CLI`, `OpenCode Desktop`, `OpenClaw`, `Crush`, or `Goose`
873
+ 3. Choose either `Install all models` or `Install selected models only`
874
+
875
+ Important behavior:
876
+
877
+ - Installs are written directly into the target tool config as FCM-managed entries, without going through `fcm-proxy`
878
+ - `Install all models` is the recommended path because FCM can refresh that catalog automatically on later launches when the provider model list changes
879
+ - `Install selected models only` is useful when you want a smaller curated picker inside the target tool
880
+ - `OpenCode CLI` and `OpenCode Desktop` share the same `opencode.json`, so the managed provider appears in both
881
+
865
882
  **Keyboard shortcuts (Settings screen β€” `P` key):**
866
883
  - **↑↓** β€” Navigate providers, maintenance row, and profile rows
867
884
  - **Enter** β€” Edit API key inline, check/install update, or load a profile
@@ -20,10 +20,11 @@
20
20
  * - Automatic config detection and model setup for both tools
21
21
  * - JSON config stored in ~/.free-coding-models.json (auto-migrates from old plain-text)
22
22
  * - Multi-provider support via sources.js (NIM/Groq/Cerebras/OpenRouter/Hugging Face/Replicate/DeepInfra/... β€” extensible)
23
- * - Settings screen (P key) to manage API keys, provider toggles, and manual updates
23
+ * - Settings screen (P key) to manage API keys, provider toggles, manual updates, and provider-key diagnostics
24
+ * - Install Endpoints flow (Y key) to push provider catalogs into OpenCode, OpenClaw, Crush, and Goose
24
25
  * - Favorites system: toggle with F, pin rows to top, persist between sessions
25
26
  * - Uptime percentage tracking (successful pings / total pings)
26
- * - Sortable columns (R/Y/O/M/L/A/S/N/H/V/B/U keys)
27
+ * - Sortable columns (R/O/M/L/A/S/C/H/V/B/U/G keys)
27
28
  * - Tier filtering via T key (cycles S+β†’Sβ†’A+β†’Aβ†’A-β†’B+β†’Bβ†’Cβ†’All)
28
29
  *
29
30
  * β†’ Functions:
@@ -118,6 +119,7 @@ import { createOverlayRenderers } from '../src/overlays.js'
118
119
  import { createKeyHandler } from '../src/key-handler.js'
119
120
  import { getToolModeOrder } from '../src/tool-metadata.js'
120
121
  import { startExternalTool } from '../src/tool-launchers.js'
122
+ import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels } from '../src/endpoint-installer.js'
121
123
 
122
124
  // πŸ“– mergedModels: cross-provider grouped model list (one entry per label, N providers each)
123
125
  // πŸ“– mergedModelByLabel: fast lookup map from display label β†’ merged model entry
@@ -310,6 +312,10 @@ async function main() {
310
312
  console.log(chalk.yellow(' OpenRouter: using cached model list (live fetch failed)'))
311
313
  }
312
314
 
315
+ // πŸ“– Re-sync tracked external-tool catalogs after the live provider catalog has settled.
316
+ // πŸ“– This keeps prior `Y` installs aligned with the current FCM model list.
317
+ refreshInstalledEndpoints(config)
318
+
313
319
  // πŸ“– Build results from MODELS β€” only include enabled providers
314
320
  // πŸ“– Each result gets providerKey so ping() knows which URL + API key to use
315
321
 
@@ -393,7 +399,8 @@ async function main() {
393
399
  settingsAddKeyMode: false, // πŸ“– Whether we're in add-key mode (append a new key to provider)
394
400
  settingsEditBuffer: '', // πŸ“– Typed characters for the API key being edited
395
401
  settingsErrorMsg: null, // πŸ“– Temporary error message to display in settings
396
- settingsTestResults: {}, // πŸ“– { providerKey: 'pending'|'ok'|'fail'|null }
402
+ settingsTestResults: {}, // πŸ“– { providerKey: 'pending'|'ok'|'auth_error'|'rate_limited'|'no_callable_model'|'fail'|'missing_key'|null }
403
+ settingsTestDetails: {}, // πŸ“– Long-form diagnostics shown under Setup Instructions after a Settings key test.
397
404
  settingsUpdateState: 'idle', // πŸ“– 'idle'|'checking'|'available'|'up-to-date'|'error'|'installing'
398
405
  settingsUpdateLatestVersion: null, // πŸ“– Latest npm version discovered from manual check
399
406
  settingsUpdateError: null, // πŸ“– Last update-check error message for maintenance row
@@ -404,6 +411,17 @@ async function main() {
404
411
  helpVisible: false, // πŸ“– Whether the help overlay (K key) is active
405
412
  settingsScrollOffset: 0, // πŸ“– Vertical scroll offset for Settings overlay viewport
406
413
  helpScrollOffset: 0, // πŸ“– Vertical scroll offset for Help overlay viewport
414
+ // πŸ“– Install Endpoints overlay state (Y key opens it)
415
+ installEndpointsOpen: false, // πŸ“– Whether the install-endpoints overlay is active
416
+ installEndpointsPhase: 'providers', // πŸ“– providers | tools | scope | models | result
417
+ installEndpointsCursor: 0, // πŸ“– Selected row within the current install phase
418
+ installEndpointsScrollOffset: 0, // πŸ“– Vertical scroll offset for the install overlay viewport
419
+ installEndpointsProviderKey: null, // πŸ“– Selected provider for endpoint installation
420
+ installEndpointsToolMode: null, // πŸ“– Selected target tool mode
421
+ installEndpointsScope: null, // πŸ“– all | selected
422
+ installEndpointsSelectedModelIds: new Set(), // πŸ“– Multi-select buffer for the selected-models phase
423
+ installEndpointsErrorMsg: null, // πŸ“– Temporary validation/error message inside the install flow
424
+ installEndpointsResult: null, // πŸ“– Final install result shown in the result phase
407
425
  // πŸ“– Smart Recommend overlay state (Q key opens it)
408
426
  recommendOpen: false, // πŸ“– Whether the recommend overlay is active
409
427
  recommendPhase: 'questionnaire', // πŸ“– 'questionnaire'|'analyzing'|'results' β€” current phase
@@ -595,7 +613,10 @@ async function main() {
595
613
  toFavoriteKey,
596
614
  getTopRecommendations,
597
615
  adjustScrollOffset,
598
- getPingModel: () => pingModel
616
+ getPingModel: () => pingModel,
617
+ getConfiguredInstallableProviders,
618
+ getInstallTargetModes,
619
+ getProviderCatalogModels,
599
620
  })
600
621
 
601
622
  onKeyPress = createKeyHandler({
@@ -616,6 +637,10 @@ async function main() {
616
637
  saveAsProfile,
617
638
  setActiveProfile,
618
639
  saveConfig,
640
+ getConfiguredInstallableProviders,
641
+ getInstallTargetModes,
642
+ getProviderCatalogModels,
643
+ installProviderEndpoints,
619
644
  syncFavoriteFlags,
620
645
  toggleFavoriteModel,
621
646
  sortResultsWithPinnedFavorites,
@@ -689,12 +714,14 @@ async function main() {
689
714
  refreshAutoPingMode()
690
715
  state.frame++
691
716
  // πŸ“– Cache visible+sorted models each frame so Enter handler always matches the display
692
- if (!state.settingsOpen && !state.recommendOpen && !state.featureRequestOpen && !state.bugReportOpen) {
717
+ if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.featureRequestOpen && !state.bugReportOpen) {
693
718
  const visible = state.results.filter(r => !r.hidden)
694
719
  state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
695
720
  }
696
721
  const content = state.settingsOpen
697
722
  ? overlays.renderSettings()
723
+ : state.installEndpointsOpen
724
+ ? overlays.renderInstallEndpoints()
698
725
  : state.recommendOpen
699
726
  ? overlays.renderRecommend()
700
727
  : state.featureRequestOpen
@@ -758,10 +785,10 @@ async function main() {
758
785
  r.status = 'up'
759
786
  } else if (code === '000') {
760
787
  r.status = 'timeout'
761
- } else if (code === '401') {
762
- // πŸ“– 401 = server is reachable but no API key set (or wrong key)
763
- // πŸ“– Treated as 'noauth' β€” server is UP, latency is real, just needs a key
764
- r.status = 'noauth'
788
+ } else if (code === '401' || code === '403') {
789
+ // πŸ“– Distinguish "no key configured" from "configured key rejected" so the
790
+ // πŸ“– Health column stays honest when Configured Only mode is enabled.
791
+ r.status = providerApiKey ? 'auth_error' : 'noauth'
765
792
  r.httpCode = code
766
793
  } else {
767
794
  r.status = 'down'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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/sources.js CHANGED
@@ -384,7 +384,7 @@ export const sources = {
384
384
  },
385
385
  codestral: {
386
386
  name: 'Codestral',
387
- url: 'https://codestral.mistral.ai/v1/chat/completions',
387
+ url: 'https://api.mistral.ai/v1/chat/completions',
388
388
  models: codestral,
389
389
  },
390
390
  hyperbolic: {
package/src/config.js CHANGED
@@ -64,7 +64,10 @@
64
64
  * "work": { "apiKeys": {...}, "providers": {...}, "favorites": [...], "settings": {...} },
65
65
  * "personal": { "apiKeys": {...}, "providers": {...}, "favorites": [...], "settings": {...} },
66
66
  * "fast": { "apiKeys": {...}, "providers": {...}, "favorites": [...], "settings": {...} }
67
- * }
67
+ * },
68
+ * "endpointInstalls": [
69
+ * { "providerKey": "nvidia", "toolMode": "opencode", "scope": "all", "modelIds": [], "lastSyncedAt": "2026-03-09T10:00:00.000Z" }
70
+ * ]
68
71
  * }
69
72
  *
70
73
  * πŸ“– Profiles store a snapshot of the user's configuration. Each profile contains:
@@ -97,11 +100,12 @@
97
100
  * β†’ setActiveProfile(config, name) β€” Set which profile is active (null to clear)
98
101
  * β†’ _emptyProfileSettings() β€” Default TUI settings for a profile
99
102
  * β†’ getProxySettings(config) β€” Return normalized proxy settings from config
103
+ * β†’ normalizeEndpointInstalls(endpointInstalls) β€” Keep tracked endpoint installs stable across app versions
100
104
  *
101
105
  * @exports loadConfig, saveConfig, getApiKey, isProviderEnabled
102
106
  * @exports addApiKey, removeApiKey, listApiKeys β€” multi-key management helpers
103
107
  * @exports saveAsProfile, loadProfile, listProfiles, deleteProfile
104
- * @exports getActiveProfileName, setActiveProfile, getProxySettings
108
+ * @exports getActiveProfileName, setActiveProfile, getProxySettings, normalizeEndpointInstalls
105
109
  * @exports CONFIG_PATH β€” path to the JSON config file
106
110
  *
107
111
  * @see bin/free-coding-models.js β€” main CLI that uses these functions
@@ -175,6 +179,7 @@ export function loadConfig() {
175
179
  if (typeof parsed.telemetry.enabled !== 'boolean') parsed.telemetry.enabled = null
176
180
  if (typeof parsed.telemetry.consentVersion !== 'number') parsed.telemetry.consentVersion = 0
177
181
  if (typeof parsed.telemetry.anonymousId !== 'string' || !parsed.telemetry.anonymousId.trim()) parsed.telemetry.anonymousId = null
182
+ parsed.endpointInstalls = normalizeEndpointInstalls(parsed.endpointInstalls)
178
183
  // πŸ“– Ensure profiles section exists (added in profile system)
179
184
  if (!parsed.profiles || typeof parsed.profiles !== 'object') parsed.profiles = {}
180
185
  for (const profile of Object.values(parsed.profiles)) {
@@ -439,6 +444,39 @@ export function getProxySettings(config) {
439
444
  return normalizeProxySettings(config?.settings?.proxy)
440
445
  }
441
446
 
447
+ /**
448
+ * πŸ“– normalizeEndpointInstalls keeps the endpoint-install tracking list safe to replay.
449
+ *
450
+ * πŸ“– Each entry represents one managed catalog install performed through the `Y` flow:
451
+ * - `providerKey`: FCM provider identifier (`nvidia`, `groq`, ...)
452
+ * - `toolMode`: canonical tool id (`opencode`, `openclaw`, `crush`, `goose`)
453
+ * - `scope`: `all` or `selected`
454
+ * - `modelIds`: only used when `scope === 'selected'`
455
+ * - `lastSyncedAt`: informational timestamp updated on successful refresh
456
+ *
457
+ * @param {unknown} endpointInstalls
458
+ * @returns {{ providerKey: string, toolMode: string, scope: 'all'|'selected', modelIds: string[], lastSyncedAt: string | null }[]}
459
+ */
460
+ export function normalizeEndpointInstalls(endpointInstalls) {
461
+ if (!Array.isArray(endpointInstalls)) return []
462
+ return endpointInstalls
463
+ .map((entry) => {
464
+ if (!entry || typeof entry !== 'object') return null
465
+ const providerKey = typeof entry.providerKey === 'string' ? entry.providerKey.trim() : ''
466
+ const toolMode = typeof entry.toolMode === 'string' ? entry.toolMode.trim() : ''
467
+ if (!providerKey || !toolMode) return null
468
+ const scope = entry.scope === 'selected' ? 'selected' : 'all'
469
+ const modelIds = Array.isArray(entry.modelIds)
470
+ ? [...new Set(entry.modelIds.filter((modelId) => typeof modelId === 'string' && modelId.trim().length > 0))]
471
+ : []
472
+ const lastSyncedAt = typeof entry.lastSyncedAt === 'string' && entry.lastSyncedAt.trim().length > 0
473
+ ? entry.lastSyncedAt
474
+ : null
475
+ return { providerKey, toolMode, scope, modelIds, lastSyncedAt }
476
+ })
477
+ .filter(Boolean)
478
+ }
479
+
442
480
  /**
443
481
  * πŸ“– saveAsProfile: Snapshot the current config state into a named profile.
444
482
  *
@@ -563,6 +601,8 @@ function _emptyConfig() {
563
601
  consentVersion: 0,
564
602
  anonymousId: null,
565
603
  },
604
+ // πŸ“– Tracked `Y` installs β€” used to refresh external tool catalogs automatically.
605
+ endpointInstalls: [],
566
606
  // πŸ“– Active profile name β€” null means no profile is loaded (using raw config).
567
607
  activeProfile: null,
568
608
  // πŸ“– Named profiles: each is a snapshot of apiKeys + providers + favorites + settings.