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/src/config.js CHANGED
@@ -59,7 +59,7 @@
59
59
  * "consentVersion": 1,
60
60
  * "anonymousId": "anon_550e8400-e29b-41d4-a716-446655440000"
61
61
  * },
62
- * "activeProfile": "work",
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
- * 📖 Profiles store a snapshot of the user's configuration. Each profile contains:
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 saveAsProfile, loadProfile, listProfiles, deleteProfile
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
- // 📖 Daemon data directory — PID file, logs, etc.
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
- function normalizeProfilesSection(profiles) {
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
- activeProfile: typeof safeConfig.activeProfile === 'string' && safeConfig.activeProfile.trim()
282
- ? safeConfig.activeProfile.trim()
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
- const replaceProfileNames = new Set((options.replaceProfileNames || []).filter((name) => typeof name === 'string' && name.length > 0))
324
- const removedProfileNames = new Set((options.removedProfileNames || []).filter((name) => typeof name === 'string' && name.length > 0))
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, favorites, or profiles they did not touch.
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
- activeProfile: normalizedIncoming.activeProfile,
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: latestConfig.activeProfile ? [latestConfig.activeProfile] : [],
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
- // ─── Config Profiles ──────────────────────────────────────────────────────────
829
+
928
830
 
929
831
  /**
930
- * 📖 _emptyProfileSettings: Default TUI settings stored in a profile.
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
- // 📖 Telemetry consent is explicit. null = not decided yet.
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
- // 📖 Active profile name — null means no profile is loaded (using raw config).
1223
- activeProfile: null,
1224
- // 📖 Named profiles: each is a snapshot of apiKeys + providers + favorites + settings.
1225
- profiles: {},
891
+ settings: _emptyProfileSettings(),
1226
892
  }
1227
893
  }