free-coding-models 0.2.0 β 0.2.2
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 +19 -2
- package/bin/free-coding-models.js +43 -14
- package/package.json +1 -1
- package/src/config.js +45 -4
- package/src/endpoint-installer.js +459 -0
- package/src/key-handler.js +344 -16
- package/src/log-reader.js +23 -2
- package/src/opencode.js +14 -2
- package/src/overlays.js +224 -8
- package/src/provider-metadata.js +3 -1
- package/src/proxy-server.js +52 -2
- package/src/render-helpers.js +14 -8
- package/src/render-table.js +18 -5
- package/src/token-stats.js +11 -1
- package/src/tool-launchers.js +50 -7
- package/src/utils.js +37 -4
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** |
|
|
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/
|
|
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
|
|
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/
|
|
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
|
|
@@ -216,9 +218,11 @@ async function main() {
|
|
|
216
218
|
// π Backward-compat: keep apiKey var for startOpenClaw() which still needs it
|
|
217
219
|
let apiKey = getApiKey(config, 'nvidia')
|
|
218
220
|
|
|
219
|
-
// π Default mode:
|
|
220
|
-
// π
|
|
221
|
-
let mode =
|
|
221
|
+
// π Default mode: use the last persisted launcher choice when valid,
|
|
222
|
+
// π otherwise fall back to OpenCode CLI.
|
|
223
|
+
let mode = getToolModeOrder().includes(config.settings?.preferredToolMode)
|
|
224
|
+
? config.settings.preferredToolMode
|
|
225
|
+
: 'opencode'
|
|
222
226
|
const requestedMode = getToolModeOrder().find((toolMode) => {
|
|
223
227
|
const flagByMode = {
|
|
224
228
|
opencode: cliArgs.openCodeMode,
|
|
@@ -308,6 +312,10 @@ async function main() {
|
|
|
308
312
|
console.log(chalk.yellow(' OpenRouter: using cached model list (live fetch failed)'))
|
|
309
313
|
}
|
|
310
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
|
+
|
|
311
319
|
// π Build results from MODELS β only include enabled providers
|
|
312
320
|
// π Each result gets providerKey so ping() knows which URL + API key to use
|
|
313
321
|
|
|
@@ -391,7 +399,8 @@ async function main() {
|
|
|
391
399
|
settingsAddKeyMode: false, // π Whether we're in add-key mode (append a new key to provider)
|
|
392
400
|
settingsEditBuffer: '', // π Typed characters for the API key being edited
|
|
393
401
|
settingsErrorMsg: null, // π Temporary error message to display in settings
|
|
394
|
-
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.
|
|
395
404
|
settingsUpdateState: 'idle', // π 'idle'|'checking'|'available'|'up-to-date'|'error'|'installing'
|
|
396
405
|
settingsUpdateLatestVersion: null, // π Latest npm version discovered from manual check
|
|
397
406
|
settingsUpdateError: null, // π Last update-check error message for maintenance row
|
|
@@ -402,6 +411,17 @@ async function main() {
|
|
|
402
411
|
helpVisible: false, // π Whether the help overlay (K key) is active
|
|
403
412
|
settingsScrollOffset: 0, // π Vertical scroll offset for Settings overlay viewport
|
|
404
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
|
|
405
425
|
// π Smart Recommend overlay state (Q key opens it)
|
|
406
426
|
recommendOpen: false, // π Whether the recommend overlay is active
|
|
407
427
|
recommendPhase: 'questionnaire', // π 'questionnaire'|'analyzing'|'results' β current phase
|
|
@@ -593,7 +613,10 @@ async function main() {
|
|
|
593
613
|
toFavoriteKey,
|
|
594
614
|
getTopRecommendations,
|
|
595
615
|
adjustScrollOffset,
|
|
596
|
-
getPingModel: () => pingModel
|
|
616
|
+
getPingModel: () => pingModel,
|
|
617
|
+
getConfiguredInstallableProviders,
|
|
618
|
+
getInstallTargetModes,
|
|
619
|
+
getProviderCatalogModels,
|
|
597
620
|
})
|
|
598
621
|
|
|
599
622
|
onKeyPress = createKeyHandler({
|
|
@@ -614,6 +637,10 @@ async function main() {
|
|
|
614
637
|
saveAsProfile,
|
|
615
638
|
setActiveProfile,
|
|
616
639
|
saveConfig,
|
|
640
|
+
getConfiguredInstallableProviders,
|
|
641
|
+
getInstallTargetModes,
|
|
642
|
+
getProviderCatalogModels,
|
|
643
|
+
installProviderEndpoints,
|
|
617
644
|
syncFavoriteFlags,
|
|
618
645
|
toggleFavoriteModel,
|
|
619
646
|
sortResultsWithPinnedFavorites,
|
|
@@ -687,12 +714,14 @@ async function main() {
|
|
|
687
714
|
refreshAutoPingMode()
|
|
688
715
|
state.frame++
|
|
689
716
|
// π Cache visible+sorted models each frame so Enter handler always matches the display
|
|
690
|
-
if (!state.settingsOpen && !state.recommendOpen && !state.featureRequestOpen && !state.bugReportOpen) {
|
|
717
|
+
if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.featureRequestOpen && !state.bugReportOpen) {
|
|
691
718
|
const visible = state.results.filter(r => !r.hidden)
|
|
692
719
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
693
720
|
}
|
|
694
721
|
const content = state.settingsOpen
|
|
695
722
|
? overlays.renderSettings()
|
|
723
|
+
: state.installEndpointsOpen
|
|
724
|
+
? overlays.renderInstallEndpoints()
|
|
696
725
|
: state.recommendOpen
|
|
697
726
|
? overlays.renderRecommend()
|
|
698
727
|
: state.featureRequestOpen
|
|
@@ -703,7 +732,7 @@ async function main() {
|
|
|
703
732
|
? overlays.renderHelp()
|
|
704
733
|
: state.logVisible
|
|
705
734
|
? overlays.renderLog()
|
|
706
|
-
: 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, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed)
|
|
735
|
+
: 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, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.settingsUpdateState, state.settingsUpdateLatestVersion, getProxySettings(state.config).enabled === true)
|
|
707
736
|
process.stdout.write(ALT_HOME + content)
|
|
708
737
|
}, Math.round(1000 / FPS))
|
|
709
738
|
|
|
@@ -711,7 +740,7 @@ async function main() {
|
|
|
711
740
|
const initialVisible = state.results.filter(r => !r.hidden)
|
|
712
741
|
state.visibleSorted = sortResultsWithPinnedFavorites(initialVisible, state.sortColumn, state.sortDirection)
|
|
713
742
|
|
|
714
|
-
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, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed))
|
|
743
|
+
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, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.settingsUpdateState, state.settingsUpdateLatestVersion, getProxySettings(state.config).enabled === true))
|
|
715
744
|
|
|
716
745
|
// π If --recommend was passed, auto-open the Smart Recommend overlay on start
|
|
717
746
|
if (cliArgs.recommendMode) {
|
|
@@ -756,10 +785,10 @@ async function main() {
|
|
|
756
785
|
r.status = 'up'
|
|
757
786
|
} else if (code === '000') {
|
|
758
787
|
r.status = 'timeout'
|
|
759
|
-
} else if (code === '401') {
|
|
760
|
-
// π
|
|
761
|
-
// π
|
|
762
|
-
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'
|
|
763
792
|
r.httpCode = code
|
|
764
793
|
} else {
|
|
765
794
|
r.status = 'down'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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/config.js
CHANGED
|
@@ -64,14 +64,17 @@
|
|
|
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:
|
|
71
74
|
* - apiKeys: API keys per provider (can differ between work/personal setups)
|
|
72
75
|
* - providers: enabled/disabled state per provider
|
|
73
76
|
* - favorites: list of pinned favorite models
|
|
74
|
-
* - settings: extra TUI preferences (tierFilter, sortColumn, sortAsc, pingInterval, hideUnconfiguredModels, proxy)
|
|
77
|
+
* - settings: extra TUI preferences (tierFilter, sortColumn, sortAsc, pingInterval, hideUnconfiguredModels, preferredToolMode, proxy)
|
|
75
78
|
*
|
|
76
79
|
* π When a profile is loaded via --profile <name> or Shift+P, the main config's
|
|
77
80
|
* apiKeys/providers/favorites are replaced with the profile's values. The profile
|
|
@@ -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)) {
|
|
@@ -395,7 +400,7 @@ export function isProviderEnabled(config, providerKey) {
|
|
|
395
400
|
* π These settings are saved/restored when switching profiles so each profile
|
|
396
401
|
* can have different sort, filter, and ping preferences.
|
|
397
402
|
*
|
|
398
|
-
* @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number, hideUnconfiguredModels: boolean }}
|
|
403
|
+
* @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number, hideUnconfiguredModels: boolean, preferredToolMode: string }}
|
|
399
404
|
*/
|
|
400
405
|
export function _emptyProfileSettings() {
|
|
401
406
|
return {
|
|
@@ -404,6 +409,7 @@ export function _emptyProfileSettings() {
|
|
|
404
409
|
sortAsc: true, // π true = ascending (fastest first for latency)
|
|
405
410
|
pingInterval: 10000, // π default ms between pings in the steady "normal" mode
|
|
406
411
|
hideUnconfiguredModels: true, // π true = default to providers that are actually configured
|
|
412
|
+
preferredToolMode: 'opencode', // π remember the last Z-selected launcher across app restarts
|
|
407
413
|
proxy: normalizeProxySettings(),
|
|
408
414
|
}
|
|
409
415
|
}
|
|
@@ -438,6 +444,39 @@ export function getProxySettings(config) {
|
|
|
438
444
|
return normalizeProxySettings(config?.settings?.proxy)
|
|
439
445
|
}
|
|
440
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
|
+
|
|
441
480
|
/**
|
|
442
481
|
* π saveAsProfile: Snapshot the current config state into a named profile.
|
|
443
482
|
*
|
|
@@ -562,6 +601,8 @@ function _emptyConfig() {
|
|
|
562
601
|
consentVersion: 0,
|
|
563
602
|
anonymousId: null,
|
|
564
603
|
},
|
|
604
|
+
// π Tracked `Y` installs β used to refresh external tool catalogs automatically.
|
|
605
|
+
endpointInstalls: [],
|
|
565
606
|
// π Active profile name β null means no profile is loaded (using raw config).
|
|
566
607
|
activeProfile: null,
|
|
567
608
|
// π Named profiles: each is a snapshot of apiKeys + providers + favorites + settings.
|