free-coding-models 0.3.9 → 0.3.12
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 +40 -0
- package/README.md +112 -1134
- package/bin/free-coding-models.js +34 -188
- package/package.json +2 -3
- package/src/cli-help.js +0 -18
- package/src/config.js +17 -351
- package/src/endpoint-installer.js +26 -64
- package/src/favorites.js +0 -14
- package/src/key-handler.js +74 -641
- package/src/legacy-proxy-cleanup.js +432 -0
- package/src/openclaw.js +69 -108
- package/src/opencode-config.js +48 -0
- package/src/opencode.js +6 -248
- package/src/overlays.js +26 -550
- package/src/product-flags.js +14 -0
- package/src/render-helpers.js +2 -34
- package/src/render-table.js +14 -33
- package/src/testfcm.js +90 -43
- package/src/token-usage-reader.js +9 -38
- package/src/tool-launchers.js +235 -409
- package/src/tool-metadata.js +0 -7
- package/src/utils.js +8 -77
- package/bin/fcm-proxy-daemon.js +0 -242
- package/src/account-manager.js +0 -634
- package/src/anthropic-translator.js +0 -440
- package/src/daemon-manager.js +0 -527
- package/src/error-classifier.js +0 -154
- package/src/log-reader.js +0 -195
- package/src/opencode-sync.js +0 -200
- package/src/proxy-server.js +0 -1477
- package/src/proxy-sync.js +0 -565
- package/src/proxy-topology.js +0 -85
- package/src/request-transformer.js +0 -180
- package/src/responses-translator.js +0 -423
- package/src/token-stats.js +0 -320
package/src/config.js
CHANGED
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
* "consentVersion": 1,
|
|
60
60
|
* "anonymousId": "anon_550e8400-e29b-41d4-a716-446655440000"
|
|
61
61
|
* },
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
* "profiles": {
|
|
64
64
|
* "work": { "apiKeys": {...}, "providers": {...}, "favorites": [...], "settings": {...} },
|
|
65
65
|
* "personal": { "apiKeys": {...}, "providers": {...}, "favorites": [...], "settings": {...} },
|
|
@@ -70,15 +70,7 @@
|
|
|
70
70
|
* ]
|
|
71
71
|
* }
|
|
72
72
|
*
|
|
73
|
-
|
|
74
|
-
* - apiKeys: API keys per provider (can differ between work/personal setups)
|
|
75
|
-
* - providers: enabled/disabled state per provider
|
|
76
|
-
* - favorites: list of pinned favorite models
|
|
77
|
-
* - settings: extra TUI preferences (tierFilter, sortColumn, sortAsc, pingInterval, hideUnconfiguredModels, preferredToolMode, proxy)
|
|
78
|
-
*
|
|
79
|
-
* 📖 When a profile is loaded via --profile <name> or Shift+P, the main config's
|
|
80
|
-
* apiKeys/providers/favorites are replaced with the profile's values. The profile
|
|
81
|
-
* data itself stays in the profiles section — it's a named snapshot, not a fork.
|
|
73
|
+
|
|
82
74
|
*
|
|
83
75
|
* 📖 Migration: On first run, if the old plain-text ~/.free-coding-models exists
|
|
84
76
|
* and the new JSON file does not, the old key is auto-migrated as the nvidia key.
|
|
@@ -95,21 +87,11 @@
|
|
|
95
87
|
* → buildPersistedConfig(incomingConfig, diskConfig, options?) — Merge a live snapshot with the latest disk state safely
|
|
96
88
|
* → replaceConfigContents(targetConfig, nextConfig) — Refresh an in-memory config object from a normalized snapshot
|
|
97
89
|
* → persistApiKeysForProvider(config, providerKey) — Persist one provider's API keys without clobbering the rest of the file
|
|
98
|
-
* → saveAsProfile(config, name) — Snapshot current apiKeys/providers/favorites/settings into a named profile
|
|
99
|
-
* → loadProfile(config, name) — Apply a named profile's values onto the live config
|
|
100
|
-
* → listProfiles(config) — Return array of profile names
|
|
101
|
-
* → deleteProfile(config, name) — Remove a named profile
|
|
102
|
-
* → getActiveProfileName(config) — Get the currently active profile name (or null)
|
|
103
|
-
* → setActiveProfile(config, name) — Set which profile is active (null to clear)
|
|
104
|
-
* → _emptyProfileSettings() — Default TUI settings for a profile
|
|
105
|
-
* → getProxySettings(config) — Return normalized proxy settings from config
|
|
106
|
-
* → setClaudeProxyModelRouting(config, modelId) — Mirror free-claude-code MODEL/MODEL_* routing onto one selected FCM model
|
|
107
90
|
* → normalizeEndpointInstalls(endpointInstalls) — Keep tracked endpoint installs stable across app versions
|
|
108
91
|
*
|
|
109
92
|
* @exports loadConfig, saveConfig, validateConfigFile, getApiKey, isProviderEnabled
|
|
110
93
|
* @exports addApiKey, removeApiKey, listApiKeys — multi-key management helpers
|
|
111
|
-
* @exports
|
|
112
|
-
* @exports getActiveProfileName, setActiveProfile, getProxySettings, setClaudeProxyModelRouting, normalizeEndpointInstalls
|
|
94
|
+
* @exports normalizeEndpointInstalls
|
|
113
95
|
* @exports buildPersistedConfig, replaceConfigContents, persistApiKeysForProvider
|
|
114
96
|
* @exports CONFIG_PATH — path to the JSON config file
|
|
115
97
|
*
|
|
@@ -118,14 +100,13 @@
|
|
|
118
100
|
*/
|
|
119
101
|
|
|
120
102
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, renameSync } from 'node:fs'
|
|
121
|
-
import { randomBytes } from 'node:crypto'
|
|
122
103
|
import { homedir } from 'node:os'
|
|
123
104
|
import { join } from 'node:path'
|
|
124
105
|
|
|
125
106
|
// 📖 New JSON config path — stores all providers' API keys + enabled state
|
|
126
107
|
export const CONFIG_PATH = join(homedir(), '.free-coding-models.json')
|
|
127
108
|
|
|
128
|
-
// 📖
|
|
109
|
+
// 📖 Runtime data directory — backups and local snapshots live here.
|
|
129
110
|
export const DAEMON_DATA_DIR = join(homedir(), '.free-coding-models')
|
|
130
111
|
|
|
131
112
|
// 📖 Old plain-text config path — used only for migration
|
|
@@ -228,7 +209,6 @@ function normalizeSettingsSection(settings) {
|
|
|
228
209
|
return {
|
|
229
210
|
...safeSettings,
|
|
230
211
|
hideUnconfiguredModels: typeof safeSettings.hideUnconfiguredModels === 'boolean' ? safeSettings.hideUnconfiguredModels : true,
|
|
231
|
-
proxy: normalizeProxySettings(safeSettings.proxy),
|
|
232
212
|
disableWidthsWarning: safeSettings.disableWidthsWarning === true,
|
|
233
213
|
}
|
|
234
214
|
}
|
|
@@ -249,25 +229,11 @@ function normalizeProfileSettings(settings) {
|
|
|
249
229
|
return {
|
|
250
230
|
..._emptyProfileSettings(),
|
|
251
231
|
...safeSettings,
|
|
252
|
-
proxy: normalizeProxySettings(safeSettings.proxy),
|
|
253
232
|
disableWidthsWarning: safeSettings.disableWidthsWarning === true,
|
|
254
233
|
}
|
|
255
234
|
}
|
|
256
235
|
|
|
257
|
-
|
|
258
|
-
if (!isPlainObject(profiles)) return {}
|
|
259
|
-
const normalized = {}
|
|
260
|
-
for (const [profileName, profile] of Object.entries(profiles)) {
|
|
261
|
-
if (!isPlainObject(profile)) continue
|
|
262
|
-
normalized[profileName] = {
|
|
263
|
-
apiKeys: normalizeApiKeysSection(profile.apiKeys),
|
|
264
|
-
providers: normalizeProvidersSection(profile.providers),
|
|
265
|
-
favorites: normalizeFavoriteList(profile.favorites),
|
|
266
|
-
settings: normalizeProfileSettings(profile.settings),
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return normalized
|
|
270
|
-
}
|
|
236
|
+
|
|
271
237
|
|
|
272
238
|
function normalizeConfigShape(config) {
|
|
273
239
|
const safeConfig = isPlainObject(config) ? config : {}
|
|
@@ -278,10 +244,8 @@ function normalizeConfigShape(config) {
|
|
|
278
244
|
favorites: normalizeFavoriteList(safeConfig.favorites),
|
|
279
245
|
telemetry: normalizeTelemetrySection(safeConfig.telemetry),
|
|
280
246
|
endpointInstalls: normalizeEndpointInstalls(safeConfig.endpointInstalls),
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
: null,
|
|
284
|
-
profiles: normalizeProfilesSection(safeConfig.profiles),
|
|
247
|
+
|
|
248
|
+
|
|
285
249
|
}
|
|
286
250
|
}
|
|
287
251
|
|
|
@@ -320,48 +284,13 @@ function mergeEndpointInstalls(diskEndpointInstalls, incomingEndpointInstalls) {
|
|
|
320
284
|
}
|
|
321
285
|
|
|
322
286
|
function mergeProfiles(diskProfiles, incomingProfiles, options = {}) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const normalizedDiskProfiles = normalizeProfilesSection(diskProfiles)
|
|
326
|
-
const normalizedIncomingProfiles = normalizeProfilesSection(incomingProfiles)
|
|
327
|
-
const mergedProfiles = {}
|
|
328
|
-
const profileNames = new Set([
|
|
329
|
-
...Object.keys(normalizedDiskProfiles),
|
|
330
|
-
...Object.keys(normalizedIncomingProfiles),
|
|
331
|
-
])
|
|
332
|
-
|
|
333
|
-
for (const profileName of profileNames) {
|
|
334
|
-
if (removedProfileNames.has(profileName)) continue
|
|
335
|
-
|
|
336
|
-
const diskProfile = normalizedDiskProfiles[profileName]
|
|
337
|
-
const incomingProfile = normalizedIncomingProfiles[profileName]
|
|
338
|
-
if (!incomingProfile) {
|
|
339
|
-
if (diskProfile) mergedProfiles[profileName] = cloneConfigValue(diskProfile)
|
|
340
|
-
continue
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (!diskProfile || replaceProfileNames.has(profileName)) {
|
|
344
|
-
mergedProfiles[profileName] = cloneConfigValue(incomingProfile)
|
|
345
|
-
continue
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
mergedProfiles[profileName] = {
|
|
349
|
-
apiKeys: { ...diskProfile.apiKeys, ...incomingProfile.apiKeys },
|
|
350
|
-
providers: { ...diskProfile.providers, ...incomingProfile.providers },
|
|
351
|
-
favorites: mergeOrderedUniqueStrings(incomingProfile.favorites, diskProfile.favorites),
|
|
352
|
-
settings: normalizeProfileSettings({
|
|
353
|
-
...diskProfile.settings,
|
|
354
|
-
...incomingProfile.settings,
|
|
355
|
-
}),
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return mergedProfiles
|
|
287
|
+
// 📖 Profile system removed - return empty object
|
|
288
|
+
return {}
|
|
360
289
|
}
|
|
361
290
|
|
|
362
291
|
/**
|
|
363
292
|
* 📖 buildPersistedConfig merges the latest disk snapshot with the in-memory config so
|
|
364
|
-
* 📖 stale writers do not accidentally wipe secrets
|
|
293
|
+
* 📖 stale writers do not accidentally wipe secrets or favorites they did not touch.
|
|
365
294
|
*
|
|
366
295
|
* @param {object} incomingConfig
|
|
367
296
|
* @param {object} [diskConfig=_emptyConfig()]
|
|
@@ -389,15 +318,8 @@ export function buildPersistedConfig(incomingConfig, diskConfig = _emptyConfig()
|
|
|
389
318
|
endpointInstalls: options.replaceEndpointInstalls === true
|
|
390
319
|
? cloneConfigValue(normalizedIncoming.endpointInstalls)
|
|
391
320
|
: mergeEndpointInstalls(normalizedDisk.endpointInstalls, normalizedIncoming.endpointInstalls),
|
|
392
|
-
|
|
393
|
-
profiles: mergeProfiles(normalizedDisk.profiles, normalizedIncoming.profiles, {
|
|
394
|
-
replaceProfileNames: options.replaceProfileNames,
|
|
395
|
-
removedProfileNames: options.removedProfileNames,
|
|
396
|
-
}),
|
|
397
|
-
}
|
|
321
|
+
// 📖 Profile system removed - always null
|
|
398
322
|
|
|
399
|
-
if (merged.activeProfile && !merged.profiles[merged.activeProfile]) {
|
|
400
|
-
merged.activeProfile = null
|
|
401
323
|
}
|
|
402
324
|
|
|
403
325
|
return normalizeConfigShape(merged)
|
|
@@ -435,32 +357,12 @@ export function persistApiKeysForProvider(config, providerKey) {
|
|
|
435
357
|
const latestConfig = readStoredConfigSnapshot()
|
|
436
358
|
const normalizedProviderValue = normalizeApiKeyValue(config?.apiKeys?.[providerKey])
|
|
437
359
|
|
|
438
|
-
latestConfig.activeProfile = typeof config?.activeProfile === 'string' && config.activeProfile.trim()
|
|
439
|
-
? config.activeProfile.trim()
|
|
440
|
-
: null
|
|
441
|
-
|
|
442
360
|
if (normalizedProviderValue === null) delete latestConfig.apiKeys[providerKey]
|
|
443
361
|
else latestConfig.apiKeys[providerKey] = cloneConfigValue(normalizedProviderValue)
|
|
444
362
|
|
|
445
|
-
if (latestConfig.activeProfile) {
|
|
446
|
-
if (!latestConfig.profiles[latestConfig.activeProfile]) {
|
|
447
|
-
latestConfig.profiles[latestConfig.activeProfile] = config?.profiles?.[latestConfig.activeProfile]
|
|
448
|
-
? cloneConfigValue(config.profiles[latestConfig.activeProfile])
|
|
449
|
-
: {
|
|
450
|
-
apiKeys: {},
|
|
451
|
-
providers: {},
|
|
452
|
-
favorites: [],
|
|
453
|
-
settings: _emptyProfileSettings(),
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (normalizedProviderValue === null) delete latestConfig.profiles[latestConfig.activeProfile].apiKeys[providerKey]
|
|
458
|
-
else latestConfig.profiles[latestConfig.activeProfile].apiKeys[providerKey] = cloneConfigValue(normalizedProviderValue)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
363
|
const saveResult = saveConfig(latestConfig, {
|
|
462
364
|
replaceApiKeys: true,
|
|
463
|
-
replaceProfileNames:
|
|
365
|
+
replaceProfileNames: [],
|
|
464
366
|
})
|
|
465
367
|
|
|
466
368
|
if (saveResult.success) replaceConfigContents(config, latestConfig)
|
|
@@ -924,13 +826,10 @@ export function isProviderEnabled(config, providerKey) {
|
|
|
924
826
|
return providerConfig.enabled !== false
|
|
925
827
|
}
|
|
926
828
|
|
|
927
|
-
|
|
829
|
+
|
|
928
830
|
|
|
929
831
|
/**
|
|
930
|
-
* 📖 _emptyProfileSettings: Default TUI settings
|
|
931
|
-
*
|
|
932
|
-
* 📖 These settings are saved/restored when switching profiles so each profile
|
|
933
|
-
* can have different sort, filter, and ping preferences.
|
|
832
|
+
* 📖 _emptyProfileSettings: Default TUI settings.
|
|
934
833
|
*
|
|
935
834
|
* @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number, hideUnconfiguredModels: boolean, preferredToolMode: string }}
|
|
936
835
|
*/
|
|
@@ -942,117 +841,10 @@ export function _emptyProfileSettings() {
|
|
|
942
841
|
pingInterval: 10000, // 📖 default ms between pings in the steady "normal" mode
|
|
943
842
|
hideUnconfiguredModels: true, // 📖 true = default to providers that are actually configured
|
|
944
843
|
preferredToolMode: 'opencode', // 📖 remember the last Z-selected launcher across app restarts
|
|
945
|
-
proxy: normalizeProxySettings(),
|
|
946
844
|
disableWidthsWarning: false, // 📖 Disable widths warning (default off)
|
|
947
845
|
}
|
|
948
846
|
}
|
|
949
847
|
|
|
950
|
-
function normalizeAnthropicRouting(anthropicRouting = null) {
|
|
951
|
-
const normalizeModelId = (value) => {
|
|
952
|
-
if (typeof value !== 'string') return null
|
|
953
|
-
const trimmed = value.trim().replace(/^fcm-proxy\//, '')
|
|
954
|
-
return trimmed || null
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
return {
|
|
958
|
-
// 📖 Mirror free-claude-code naming: MODEL is the fallback, and MODEL_* are
|
|
959
|
-
// 📖 Claude-family overrides. FCM currently pins all four to one selected model.
|
|
960
|
-
model: normalizeModelId(anthropicRouting?.model),
|
|
961
|
-
modelOpus: normalizeModelId(anthropicRouting?.modelOpus),
|
|
962
|
-
modelSonnet: normalizeModelId(anthropicRouting?.modelSonnet),
|
|
963
|
-
modelHaiku: normalizeModelId(anthropicRouting?.modelHaiku),
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
/**
|
|
968
|
-
* 📖 normalizeProxySettings: keep proxy-related preferences stable across old configs,
|
|
969
|
-
* 📖 new installs, and profile switches. Proxy is opt-in by default.
|
|
970
|
-
*
|
|
971
|
-
* 📖 stableToken — persisted bearer token shared between TUI and daemon. Generated once
|
|
972
|
-
* on first access so env files and tool configs remain valid across restarts.
|
|
973
|
-
* 📖 daemonEnabled — opt-in for the always-on background proxy daemon (launchd / systemd).
|
|
974
|
-
* 📖 daemonConsent — ISO timestamp of when user consented to daemon install, or null.
|
|
975
|
-
*
|
|
976
|
-
* @param {object|undefined|null} proxy
|
|
977
|
-
* @returns {{ enabled: boolean, syncToOpenCode: boolean, preferredPort: number, stableToken: string, daemonEnabled: boolean, daemonConsent: string|null, anthropicRouting: { model: string|null, modelOpus: string|null, modelSonnet: string|null, modelHaiku: string|null } }}
|
|
978
|
-
*/
|
|
979
|
-
export function normalizeProxySettings(proxy = null) {
|
|
980
|
-
const preferredPort = Number.isInteger(proxy?.preferredPort) && proxy.preferredPort >= 0 && proxy.preferredPort <= 65535
|
|
981
|
-
? proxy.preferredPort
|
|
982
|
-
: 0
|
|
983
|
-
|
|
984
|
-
// 📖 Generate a stable proxy token once and persist it forever
|
|
985
|
-
const stableToken = (typeof proxy?.stableToken === 'string' && proxy.stableToken.length > 0)
|
|
986
|
-
? proxy.stableToken
|
|
987
|
-
: `fcm_${randomBytes(24).toString('hex')}`
|
|
988
|
-
|
|
989
|
-
return {
|
|
990
|
-
enabled: proxy?.enabled === true,
|
|
991
|
-
syncToOpenCode: proxy?.syncToOpenCode === true,
|
|
992
|
-
preferredPort,
|
|
993
|
-
stableToken,
|
|
994
|
-
daemonEnabled: proxy?.daemonEnabled === true,
|
|
995
|
-
daemonConsent: (typeof proxy?.daemonConsent === 'string' && proxy.daemonConsent.length > 0)
|
|
996
|
-
? proxy.daemonConsent
|
|
997
|
-
: null,
|
|
998
|
-
anthropicRouting: normalizeAnthropicRouting(proxy?.anthropicRouting),
|
|
999
|
-
// 📖 activeTool — legacy field kept only for backward compatibility.
|
|
1000
|
-
// 📖 Runtime sync now follows the current Z-selected tool automatically.
|
|
1001
|
-
activeTool: (typeof proxy?.activeTool === 'string' && proxy.activeTool.length > 0)
|
|
1002
|
-
? proxy.activeTool
|
|
1003
|
-
: null,
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
/**
|
|
1008
|
-
* 📖 getProxySettings: return normalized proxy settings from the live config.
|
|
1009
|
-
* 📖 This centralizes the opt-in default so launchers do not guess.
|
|
1010
|
-
*
|
|
1011
|
-
* @param {object} config
|
|
1012
|
-
* @returns {{ enabled: boolean, syncToOpenCode: boolean, preferredPort: number }}
|
|
1013
|
-
*/
|
|
1014
|
-
export function getProxySettings(config) {
|
|
1015
|
-
return normalizeProxySettings(config?.settings?.proxy)
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
/**
|
|
1019
|
-
* 📖 Persist the free-claude-code style MODEL / MODEL_OPUS / MODEL_SONNET /
|
|
1020
|
-
* 📖 MODEL_HAIKU routing onto one selected proxy model. Claude Code itself then
|
|
1021
|
-
* 📖 keeps speaking in fake Claude model ids while the proxy chooses the backend.
|
|
1022
|
-
*
|
|
1023
|
-
* @param {object} config
|
|
1024
|
-
* @param {string} modelId
|
|
1025
|
-
* @returns {boolean} true when the normalized proxy settings changed
|
|
1026
|
-
*/
|
|
1027
|
-
export function setClaudeProxyModelRouting(config, modelId) {
|
|
1028
|
-
const normalizedModelId = typeof modelId === 'string' ? modelId.trim().replace(/^fcm-proxy\//, '') : ''
|
|
1029
|
-
if (!normalizedModelId) return false
|
|
1030
|
-
|
|
1031
|
-
if (!config.settings || typeof config.settings !== 'object') config.settings = {}
|
|
1032
|
-
|
|
1033
|
-
const current = getProxySettings(config)
|
|
1034
|
-
const nextAnthropicRouting = {
|
|
1035
|
-
model: normalizedModelId,
|
|
1036
|
-
modelOpus: normalizedModelId,
|
|
1037
|
-
modelSonnet: normalizedModelId,
|
|
1038
|
-
modelHaiku: normalizedModelId,
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
const changed = current.enabled !== true
|
|
1042
|
-
|| current.anthropicRouting.model !== nextAnthropicRouting.model
|
|
1043
|
-
|| current.anthropicRouting.modelOpus !== nextAnthropicRouting.modelOpus
|
|
1044
|
-
|| current.anthropicRouting.modelSonnet !== nextAnthropicRouting.modelSonnet
|
|
1045
|
-
|| current.anthropicRouting.modelHaiku !== nextAnthropicRouting.modelHaiku
|
|
1046
|
-
|
|
1047
|
-
config.settings.proxy = {
|
|
1048
|
-
...current,
|
|
1049
|
-
enabled: true,
|
|
1050
|
-
anthropicRouting: nextAnthropicRouting,
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
return changed
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
848
|
/**
|
|
1057
849
|
* 📖 normalizeEndpointInstalls keeps the endpoint-install tracking list safe to replay.
|
|
1058
850
|
*
|
|
@@ -1086,142 +878,16 @@ export function normalizeEndpointInstalls(endpointInstalls) {
|
|
|
1086
878
|
.filter(Boolean)
|
|
1087
879
|
}
|
|
1088
880
|
|
|
1089
|
-
|
|
1090
|
-
* 📖 saveAsProfile: Snapshot the current config state into a named profile.
|
|
1091
|
-
*
|
|
1092
|
-
* 📖 Takes the current apiKeys, providers, favorites, plus explicit TUI settings
|
|
1093
|
-
* and stores them under config.profiles[name]. Does NOT change activeProfile —
|
|
1094
|
-
* call setActiveProfile() separately if you want to switch to this profile.
|
|
1095
|
-
*
|
|
1096
|
-
* 📖 If a profile with the same name exists, it's overwritten.
|
|
1097
|
-
*
|
|
1098
|
-
* @param {object} config — Live config object (will be mutated)
|
|
1099
|
-
* @param {string} name — Profile name (e.g. 'work', 'personal', 'fast')
|
|
1100
|
-
* @param {object} [settings] — TUI settings to save (tierFilter, sortColumn, etc.)
|
|
1101
|
-
* @returns {object} The config object (for chaining)
|
|
1102
|
-
*/
|
|
1103
|
-
export function saveAsProfile(config, name, settings = null) {
|
|
1104
|
-
if (!config.profiles || typeof config.profiles !== 'object') config.profiles = {}
|
|
1105
|
-
config.profiles[name] = {
|
|
1106
|
-
apiKeys: JSON.parse(JSON.stringify(config.apiKeys || {})),
|
|
1107
|
-
providers: JSON.parse(JSON.stringify(config.providers || {})),
|
|
1108
|
-
favorites: [...(config.favorites || [])],
|
|
1109
|
-
settings: settings ? { ..._emptyProfileSettings(), ...settings } : _emptyProfileSettings(),
|
|
1110
|
-
}
|
|
1111
|
-
return config
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
/**
|
|
1115
|
-
* 📖 loadProfile: Apply a named profile's values onto the live config.
|
|
1116
|
-
*
|
|
1117
|
-
* 📖 Replaces config.apiKeys, config.providers, config.favorites with the
|
|
1118
|
-
* profile's stored values. Also sets config.activeProfile to the loaded name.
|
|
1119
|
-
*
|
|
1120
|
-
* 📖 Returns the profile's TUI settings so the caller (main CLI) can apply them
|
|
1121
|
-
* to the live state object (sortColumn, tierFilter, etc.).
|
|
1122
|
-
*
|
|
1123
|
-
* 📖 If the profile doesn't exist, returns null (caller should show an error).
|
|
1124
|
-
*
|
|
1125
|
-
* @param {object} config — Live config object (will be mutated)
|
|
1126
|
-
* @param {string} name — Profile name to load
|
|
1127
|
-
* @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number }|null}
|
|
1128
|
-
* The profile's TUI settings, or null if profile not found
|
|
1129
|
-
*/
|
|
1130
|
-
export function loadProfile(config, name) {
|
|
1131
|
-
const profile = config?.profiles?.[name]
|
|
1132
|
-
if (!profile) return null
|
|
1133
|
-
const nextSettings = profile.settings ? { ..._emptyProfileSettings(), ...profile.settings, proxy: normalizeProxySettings(profile.settings.proxy) } : _emptyProfileSettings()
|
|
1134
|
-
|
|
1135
|
-
// 📖 Deep-copy the profile data into the live config (don't share references)
|
|
1136
|
-
// 📖 IMPORTANT: MERGE apiKeys instead of replacing to preserve keys not in profile
|
|
1137
|
-
// 📖 Profile keys take priority over existing keys (allows profile-specific overrides)
|
|
1138
|
-
const profileApiKeys = profile.apiKeys || {}
|
|
1139
|
-
const mergedApiKeys = { ...config.apiKeys || {}, ...profileApiKeys }
|
|
1140
|
-
config.apiKeys = JSON.parse(JSON.stringify(mergedApiKeys))
|
|
1141
|
-
|
|
1142
|
-
// 📖 For providers, favorites: replace with profile values (these are profile-specific settings)
|
|
1143
|
-
config.providers = JSON.parse(JSON.stringify(profile.providers || {}))
|
|
1144
|
-
config.favorites = [...(profile.favorites || [])]
|
|
1145
|
-
config.settings = nextSettings
|
|
1146
|
-
config.activeProfile = name
|
|
1147
|
-
|
|
1148
|
-
return nextSettings
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
/**
|
|
1152
|
-
* 📖 listProfiles: Get all saved profile names.
|
|
1153
|
-
*
|
|
1154
|
-
* @param {object} config
|
|
1155
|
-
* @returns {string[]} Array of profile names, sorted alphabetically
|
|
1156
|
-
*/
|
|
1157
|
-
export function listProfiles(config) {
|
|
1158
|
-
if (!config?.profiles || typeof config.profiles !== 'object') return []
|
|
1159
|
-
return Object.keys(config.profiles).sort()
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
/**
|
|
1163
|
-
* 📖 deleteProfile: Remove a named profile from the config.
|
|
1164
|
-
*
|
|
1165
|
-
* 📖 If the deleted profile is the active one, clears activeProfile.
|
|
1166
|
-
*
|
|
1167
|
-
* @param {object} config — Live config object (will be mutated)
|
|
1168
|
-
* @param {string} name — Profile name to delete
|
|
1169
|
-
* @returns {boolean} True if the profile existed and was deleted
|
|
1170
|
-
*/
|
|
1171
|
-
export function deleteProfile(config, name) {
|
|
1172
|
-
if (!config?.profiles?.[name]) return false
|
|
1173
|
-
delete config.profiles[name]
|
|
1174
|
-
if (config.activeProfile === name) config.activeProfile = null
|
|
1175
|
-
return true
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
/**
|
|
1179
|
-
* 📖 getActiveProfileName: Get the currently active profile name.
|
|
1180
|
-
*
|
|
1181
|
-
* @param {object} config
|
|
1182
|
-
* @returns {string|null} Profile name, or null if no profile is active
|
|
1183
|
-
*/
|
|
1184
|
-
export function getActiveProfileName(config) {
|
|
1185
|
-
return config?.activeProfile || null
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
/**
|
|
1189
|
-
* 📖 setActiveProfile: Set which profile is active (or null to clear).
|
|
1190
|
-
*
|
|
1191
|
-
* 📖 This just stores the name — it does NOT load the profile's data.
|
|
1192
|
-
* Call loadProfile() first to actually apply the profile's values.
|
|
1193
|
-
*
|
|
1194
|
-
* @param {object} config — Live config object (will be mutated)
|
|
1195
|
-
* @param {string|null} name — Profile name, or null to clear
|
|
1196
|
-
*/
|
|
1197
|
-
export function setActiveProfile(config, name) {
|
|
1198
|
-
config.activeProfile = name || null
|
|
1199
|
-
}
|
|
881
|
+
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1200
882
|
|
|
1201
883
|
// 📖 Internal helper: create a blank config with the right shape
|
|
1202
884
|
function _emptyConfig() {
|
|
1203
885
|
return {
|
|
1204
886
|
apiKeys: {},
|
|
1205
887
|
providers: {},
|
|
1206
|
-
// 📖 Global TUI preferences that should persist even without a named profile.
|
|
1207
|
-
settings: {
|
|
1208
|
-
hideUnconfiguredModels: true,
|
|
1209
|
-
proxy: normalizeProxySettings(),
|
|
1210
|
-
disableWidthsWarning: false, // 📖 Disable widths warning toggle (default off)
|
|
1211
|
-
},
|
|
1212
|
-
// 📖 Pinned favorites rendered at top of the table ("providerKey/modelId").
|
|
1213
888
|
favorites: [],
|
|
1214
|
-
|
|
1215
|
-
telemetry: {
|
|
1216
|
-
enabled: null,
|
|
1217
|
-
consentVersion: 0,
|
|
1218
|
-
anonymousId: null,
|
|
1219
|
-
},
|
|
1220
|
-
// 📖 Tracked `Y` installs — used to refresh external tool catalogs automatically.
|
|
889
|
+
telemetry: { enabled: null, consentVersion: 0, anonymousId: null },
|
|
1221
890
|
endpointInstalls: [],
|
|
1222
|
-
|
|
1223
|
-
activeProfile: null,
|
|
1224
|
-
// 📖 Named profiles: each is a snapshot of apiKeys + providers + favorites + settings.
|
|
1225
|
-
profiles: {},
|
|
891
|
+
settings: _emptyProfileSettings(),
|
|
1226
892
|
}
|
|
1227
893
|
}
|