free-coding-models 0.3.25 → 0.3.28
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 +62 -2
- package/README.md +23 -1
- package/package.json +1 -1
- package/sources.js +37 -19
- package/src/app.js +33 -6
- package/src/command-palette.js +1 -0
- package/src/installed-models-manager.js +636 -0
- package/src/key-handler.js +187 -14
- package/src/overlays.js +111 -2
- package/src/render-table.js +26 -10
- package/src/updater.js +127 -36
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/installed-models-manager.js
|
|
3
|
+
* @description Scan, parse, and manage models configured in external tool configs.
|
|
4
|
+
*
|
|
5
|
+
* @details
|
|
6
|
+
* 📖 This module provides functions to:
|
|
7
|
+
* - Scan all supported tool configs for installed models
|
|
8
|
+
* - Parse tool-specific config files (YAML, JSON)
|
|
9
|
+
* - Soft-delete models with backup to ~/.free-coding-models-backups.json
|
|
10
|
+
* - Launch tools with selected models
|
|
11
|
+
* - Reinstall FCM endpoints for providers
|
|
12
|
+
*
|
|
13
|
+
* 📖 Supported tools:
|
|
14
|
+
* - Goose (~/.config/goose/config.yaml + custom_providers/*.json)
|
|
15
|
+
* - Crush (~/.config/crush/crush.json)
|
|
16
|
+
* - Aider (~/.aider.conf.yml)
|
|
17
|
+
* - Qwen (~/.qwen/settings.json)
|
|
18
|
+
* - Pi (~/.pi/agent/models.json + settings.json)
|
|
19
|
+
* - OpenHands (~/.fcm-openhands-env)
|
|
20
|
+
* - Amp (~/.config/amp/settings.json)
|
|
21
|
+
*
|
|
22
|
+
* 📖 Backup system:
|
|
23
|
+
* - Disabled models are saved to ~/.free-coding-models-backups.json
|
|
24
|
+
* - Each entry includes: toolMode, modelId, originalConfig, configPath, disabledAt
|
|
25
|
+
*
|
|
26
|
+
* @functions
|
|
27
|
+
* → scanAllToolConfigs — Scan all tool configs and return structured results
|
|
28
|
+
* → parseToolConfig — Parse a specific tool's config file
|
|
29
|
+
* → parseGooseConfig — Parse Goose YAML config
|
|
30
|
+
* → parseCrushConfig — Parse Crush JSON config
|
|
31
|
+
* → parseAiderConfig — Parse Aider YAML config
|
|
32
|
+
* → parseQwenConfig — Parse Qwen JSON config
|
|
33
|
+
* → parsePiConfig — Parse Pi JSON configs
|
|
34
|
+
* → parseOpenHandsConfig — Parse OpenHands env file
|
|
35
|
+
* → parseAmpConfig — Parse Amp JSON config
|
|
36
|
+
* → softDeleteModel — Remove model from config with backup
|
|
37
|
+
* → launchToolWithModel — Launch tool with specific model
|
|
38
|
+
* → reinstallEndpoint — Reinstall FCM endpoint for provider
|
|
39
|
+
*
|
|
40
|
+
* @exports scanAllToolConfigs, softDeleteModel, launchToolWithModel, reinstallEndpoint
|
|
41
|
+
*
|
|
42
|
+
* @see src/tool-launchers.js — for launch functions
|
|
43
|
+
* @see src/endpoint-installer.js — for reinstall logic
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
47
|
+
import { homedir } from 'node:os'
|
|
48
|
+
import { join, dirname } from 'node:path'
|
|
49
|
+
import { sources } from '../sources.js'
|
|
50
|
+
|
|
51
|
+
const BACKUP_PATH = join(homedir(), '.free-coding-models-backups.json')
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 📖 Get tool config paths
|
|
55
|
+
*/
|
|
56
|
+
function getToolConfigPaths(homeDir = homedir()) {
|
|
57
|
+
return {
|
|
58
|
+
goose: join(homeDir, '.config', 'goose', 'config.yaml'),
|
|
59
|
+
crush: join(homeDir, '.config', 'crush', 'crush.json'),
|
|
60
|
+
aider: join(homeDir, '.aider.conf.yml'),
|
|
61
|
+
qwen: join(homeDir, '.qwen', 'settings.json'),
|
|
62
|
+
piModels: join(homeDir, '.pi', 'agent', 'models.json'),
|
|
63
|
+
piSettings: join(homeDir, '.pi', 'agent', 'settings.json'),
|
|
64
|
+
openHands: join(homeDir, '.fcm-openhands-env'),
|
|
65
|
+
amp: join(homeDir, '.config', 'amp', 'settings.json'),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 📖 Simple YAML parser for Goose and Aider configs
|
|
71
|
+
* Handles basic key: value and multiline strings
|
|
72
|
+
*/
|
|
73
|
+
function parseSimpleYaml(filePath) {
|
|
74
|
+
if (!existsSync(filePath)) return null
|
|
75
|
+
try {
|
|
76
|
+
const content = readFileSync(filePath, 'utf8')
|
|
77
|
+
const result = {}
|
|
78
|
+
let currentKey = null
|
|
79
|
+
|
|
80
|
+
for (const line of content.split('\n')) {
|
|
81
|
+
const trimmed = line.trim()
|
|
82
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
83
|
+
|
|
84
|
+
const colonIndex = trimmed.indexOf(':')
|
|
85
|
+
if (colonIndex === -1) {
|
|
86
|
+
if (currentKey && result[currentKey] !== undefined) {
|
|
87
|
+
result[currentKey] += '\n' + trimmed
|
|
88
|
+
}
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
currentKey = trimmed.slice(0, colonIndex).trim()
|
|
93
|
+
const value = trimmed.slice(colonIndex + 1).trim()
|
|
94
|
+
|
|
95
|
+
if (value === '' || value === '|') {
|
|
96
|
+
result[currentKey] = ''
|
|
97
|
+
} else if (value.startsWith('"') || value.startsWith("'")) {
|
|
98
|
+
result[currentKey] = value.slice(1, -1)
|
|
99
|
+
} else {
|
|
100
|
+
result[currentKey] = value
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result
|
|
105
|
+
} catch (err) {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 📖 Parse Goose config for GOOSE_MODEL
|
|
112
|
+
*/
|
|
113
|
+
function parseGooseConfig(paths = getToolConfigPaths()) {
|
|
114
|
+
const configPath = paths.goose
|
|
115
|
+
if (!existsSync(configPath)) {
|
|
116
|
+
return { isValid: false, models: [], configPath }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const yaml = parseSimpleYaml(configPath)
|
|
121
|
+
if (!yaml) {
|
|
122
|
+
return { isValid: false, models: [], configPath }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const gooseModel = yaml['GOOSE_MODEL']
|
|
126
|
+
const gooseProvider = yaml['GOOSE_PROVIDER']
|
|
127
|
+
|
|
128
|
+
const models = []
|
|
129
|
+
if (gooseModel) {
|
|
130
|
+
models.push({
|
|
131
|
+
modelId: gooseModel,
|
|
132
|
+
label: gooseModel,
|
|
133
|
+
tier: '-',
|
|
134
|
+
sweScore: '-',
|
|
135
|
+
providerKey: 'external',
|
|
136
|
+
isExternal: true,
|
|
137
|
+
canLaunch: true,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
isValid: true,
|
|
143
|
+
hasManagedMarker: yaml['GOOSE_PROVIDER']?.startsWith('fcm-'),
|
|
144
|
+
models,
|
|
145
|
+
configPath,
|
|
146
|
+
}
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return { isValid: false, models: [], configPath }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 📖 Parse Crush config for models.large/small
|
|
154
|
+
*/
|
|
155
|
+
function parseCrushConfig(paths = getToolConfigPaths()) {
|
|
156
|
+
const configPath = paths.crush
|
|
157
|
+
if (!existsSync(configPath)) {
|
|
158
|
+
return { isValid: false, models: [], configPath }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const content = readFileSync(configPath, 'utf8')
|
|
163
|
+
const config = JSON.parse(content)
|
|
164
|
+
|
|
165
|
+
const models = []
|
|
166
|
+
// Extract models from providers section
|
|
167
|
+
if (config.providers) {
|
|
168
|
+
for (const providerKey in config.providers) {
|
|
169
|
+
const provider = config.providers[providerKey]
|
|
170
|
+
if (provider.models) {
|
|
171
|
+
for (const model of provider.models) {
|
|
172
|
+
models.push({
|
|
173
|
+
modelId: model.id,
|
|
174
|
+
label: model.name || model.id,
|
|
175
|
+
tier: '-',
|
|
176
|
+
sweScore: '-',
|
|
177
|
+
providerKey: 'external',
|
|
178
|
+
isExternal: true,
|
|
179
|
+
canLaunch: true,
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Extract models from models section (large/small)
|
|
187
|
+
if (config.models?.large?.model) {
|
|
188
|
+
models.push({
|
|
189
|
+
modelId: config.models.large.model,
|
|
190
|
+
label: `${config.models.large.model} (large)`,
|
|
191
|
+
tier: '-',
|
|
192
|
+
sweScore: '-',
|
|
193
|
+
providerKey: 'external',
|
|
194
|
+
isExternal: true,
|
|
195
|
+
canLaunch: true,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
if (config.models?.small?.model) {
|
|
199
|
+
models.push({
|
|
200
|
+
modelId: config.models.small.model,
|
|
201
|
+
label: `${config.models.small.model} (small)`,
|
|
202
|
+
tier: '-',
|
|
203
|
+
sweScore: '-',
|
|
204
|
+
providerKey: 'external',
|
|
205
|
+
isExternal: true,
|
|
206
|
+
canLaunch: true,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
if (config.models?.small?.model) {
|
|
210
|
+
models.push({
|
|
211
|
+
modelId: config.models.small.model + '-small',
|
|
212
|
+
label: `${config.models.small.model} (small)`,
|
|
213
|
+
tier: '-',
|
|
214
|
+
sweScore: '-',
|
|
215
|
+
providerKey: 'external',
|
|
216
|
+
isExternal: true,
|
|
217
|
+
canLaunch: true,
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
isValid: true,
|
|
223
|
+
hasManagedMarker: config.providers?.freeCodingModels !== undefined,
|
|
224
|
+
models,
|
|
225
|
+
configPath,
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
return { isValid: false, models: [], configPath }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 📖 Parse Aider config for model
|
|
234
|
+
*/
|
|
235
|
+
function parseAiderConfig(paths = getToolConfigPaths()) {
|
|
236
|
+
const configPath = paths.aider
|
|
237
|
+
if (!existsSync(configPath)) {
|
|
238
|
+
return { isValid: false, models: [], configPath }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const yaml = parseSimpleYaml(configPath)
|
|
243
|
+
if (!yaml) {
|
|
244
|
+
return { isValid: false, models: [], configPath }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const models = []
|
|
248
|
+
const aiderModel = yaml['model']
|
|
249
|
+
if (aiderModel) {
|
|
250
|
+
const modelId = aiderModel.startsWith('openai/') ? aiderModel.slice(7) : aiderModel
|
|
251
|
+
models.push({
|
|
252
|
+
modelId,
|
|
253
|
+
label: modelId,
|
|
254
|
+
tier: '-',
|
|
255
|
+
sweScore: '-',
|
|
256
|
+
providerKey: 'external',
|
|
257
|
+
isExternal: true,
|
|
258
|
+
canLaunch: true,
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
isValid: true,
|
|
264
|
+
hasManagedMarker: yaml['openai-api-base']?.includes('build.nvidia.com') || false,
|
|
265
|
+
models,
|
|
266
|
+
configPath,
|
|
267
|
+
}
|
|
268
|
+
} catch (err) {
|
|
269
|
+
return { isValid: false, models: [], configPath }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 📖 Parse Qwen config for model
|
|
275
|
+
*/
|
|
276
|
+
function parseQwenConfig(paths = getToolConfigPaths()) {
|
|
277
|
+
const configPath = paths.qwen
|
|
278
|
+
if (!existsSync(configPath)) {
|
|
279
|
+
return { isValid: false, models: [], configPath }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const content = readFileSync(configPath, 'utf8')
|
|
284
|
+
const config = JSON.parse(content)
|
|
285
|
+
|
|
286
|
+
const models = []
|
|
287
|
+
if (config.model) {
|
|
288
|
+
models.push({
|
|
289
|
+
modelId: config.model,
|
|
290
|
+
label: config.model,
|
|
291
|
+
tier: '-',
|
|
292
|
+
sweScore: '-',
|
|
293
|
+
providerKey: 'external',
|
|
294
|
+
isExternal: true,
|
|
295
|
+
canLaunch: true,
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
isValid: true,
|
|
301
|
+
hasManagedMarker: Array.isArray(config.modelProviders?.openai) && config.modelProviders.openai.length > 0,
|
|
302
|
+
models,
|
|
303
|
+
configPath,
|
|
304
|
+
}
|
|
305
|
+
} catch (err) {
|
|
306
|
+
return { isValid: false, models: [], configPath }
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 📖 Parse Pi configs for defaultModel
|
|
312
|
+
*/
|
|
313
|
+
function parsePiConfig(paths = getToolConfigPaths()) {
|
|
314
|
+
const settingsPath = paths.piSettings
|
|
315
|
+
if (!existsSync(settingsPath)) {
|
|
316
|
+
return { isValid: false, models: [], configPath: settingsPath }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const content = readFileSync(settingsPath, 'utf8')
|
|
321
|
+
const config = JSON.parse(content)
|
|
322
|
+
|
|
323
|
+
const models = []
|
|
324
|
+
if (config.defaultModel && config.defaultProvider) {
|
|
325
|
+
models.push({
|
|
326
|
+
modelId: config.defaultModel,
|
|
327
|
+
label: config.defaultModel,
|
|
328
|
+
tier: '-',
|
|
329
|
+
sweScore: '-',
|
|
330
|
+
providerKey: 'external',
|
|
331
|
+
isExternal: true,
|
|
332
|
+
canLaunch: true,
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
isValid: true,
|
|
338
|
+
hasManagedMarker: config.defaultProvider === 'freeCodingModels',
|
|
339
|
+
models,
|
|
340
|
+
configPath: settingsPath,
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
return { isValid: false, models: [], configPath: settingsPath }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 📖 Parse OpenHands env file for LLM_MODEL
|
|
349
|
+
*/
|
|
350
|
+
function parseOpenHandsConfig(paths = getToolConfigPaths()) {
|
|
351
|
+
const configPath = paths.openHands
|
|
352
|
+
if (!existsSync(configPath)) {
|
|
353
|
+
return { isValid: false, models: [], configPath }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const content = readFileSync(configPath, 'utf8')
|
|
358
|
+
const models = []
|
|
359
|
+
|
|
360
|
+
for (const line of content.split('\n')) {
|
|
361
|
+
const trimmed = line.trim()
|
|
362
|
+
if (trimmed.startsWith('export LLM_MODEL="') || trimmed.startsWith('export LLM_MODEL=\'')) {
|
|
363
|
+
const match = trimmed.match(/export LLM_MODEL=(["'])(.*?)\1/)
|
|
364
|
+
if (match) {
|
|
365
|
+
models.push({
|
|
366
|
+
modelId: match[2],
|
|
367
|
+
label: match[2],
|
|
368
|
+
tier: '-',
|
|
369
|
+
sweScore: '-',
|
|
370
|
+
providerKey: 'external',
|
|
371
|
+
isExternal: true,
|
|
372
|
+
canLaunch: true,
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
isValid: true,
|
|
380
|
+
hasManagedMarker: content.includes('Managed by free-coding-models'),
|
|
381
|
+
models,
|
|
382
|
+
configPath,
|
|
383
|
+
}
|
|
384
|
+
} catch (err) {
|
|
385
|
+
return { isValid: false, models: [], configPath }
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* 📖 Parse Amp config for amp.model
|
|
391
|
+
*/
|
|
392
|
+
function parseAmpConfig(paths = getToolConfigPaths()) {
|
|
393
|
+
const configPath = paths.amp
|
|
394
|
+
if (!existsSync(configPath)) {
|
|
395
|
+
return { isValid: false, models: [], configPath }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const content = readFileSync(configPath, 'utf8')
|
|
400
|
+
const config = JSON.parse(content)
|
|
401
|
+
|
|
402
|
+
const models = []
|
|
403
|
+
if (config['amp.model']) {
|
|
404
|
+
models.push({
|
|
405
|
+
modelId: config['amp.model'],
|
|
406
|
+
label: config['amp.model'],
|
|
407
|
+
tier: '-',
|
|
408
|
+
sweScore: '-',
|
|
409
|
+
providerKey: 'external',
|
|
410
|
+
isExternal: true,
|
|
411
|
+
canLaunch: true,
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
isValid: true,
|
|
417
|
+
hasManagedMarker: config['amp.url']?.includes('build.nvidia.com') || false,
|
|
418
|
+
models,
|
|
419
|
+
configPath,
|
|
420
|
+
}
|
|
421
|
+
} catch (err) {
|
|
422
|
+
return { isValid: false, models: [], configPath }
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* 📖 Enhance model with metadata from sources.js
|
|
428
|
+
*/
|
|
429
|
+
function enhanceModelMetadata(model) {
|
|
430
|
+
const modelId = model.modelId
|
|
431
|
+
|
|
432
|
+
for (const providerKey in sources) {
|
|
433
|
+
const provider = sources[providerKey]
|
|
434
|
+
for (const m of provider.models) {
|
|
435
|
+
if (m[0] === modelId) {
|
|
436
|
+
return {
|
|
437
|
+
...model,
|
|
438
|
+
label: m[1],
|
|
439
|
+
tier: m[2],
|
|
440
|
+
sweScore: m[3],
|
|
441
|
+
providerKey,
|
|
442
|
+
isExternal: false,
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return model
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 📖 Parse a specific tool's config
|
|
453
|
+
*/
|
|
454
|
+
export function parseToolConfig(toolMode, paths = getToolConfigPaths()) {
|
|
455
|
+
switch (toolMode) {
|
|
456
|
+
case 'goose':
|
|
457
|
+
return parseGooseConfig(paths)
|
|
458
|
+
case 'crush':
|
|
459
|
+
return parseCrushConfig(paths)
|
|
460
|
+
case 'aider':
|
|
461
|
+
return parseAiderConfig(paths)
|
|
462
|
+
case 'qwen':
|
|
463
|
+
return parseQwenConfig(paths)
|
|
464
|
+
case 'pi':
|
|
465
|
+
return parsePiConfig(paths)
|
|
466
|
+
case 'openhands':
|
|
467
|
+
return parseOpenHandsConfig(paths)
|
|
468
|
+
case 'amp':
|
|
469
|
+
return parseAmpConfig(paths)
|
|
470
|
+
default:
|
|
471
|
+
return { isValid: false, models: [], configPath: '' }
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* 📖 Scan all tool configs and return structured results
|
|
477
|
+
*/
|
|
478
|
+
export function scanAllToolConfigs(paths = getToolConfigPaths()) {
|
|
479
|
+
const toolModes = ['goose', 'crush', 'aider', 'qwen', 'pi', 'openhands', 'amp']
|
|
480
|
+
|
|
481
|
+
return toolModes.map((toolMode) => {
|
|
482
|
+
const result = parseToolConfig(toolMode, paths)
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
toolMode,
|
|
486
|
+
toolLabel: toolMode.charAt(0).toUpperCase() + toolMode.slice(1),
|
|
487
|
+
toolEmoji: getToolEmoji(toolMode),
|
|
488
|
+
...result,
|
|
489
|
+
models: result.models.map(enhanceModelMetadata),
|
|
490
|
+
}
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* 📖 Get tool emoji
|
|
496
|
+
*/
|
|
497
|
+
function getToolEmoji(toolMode) {
|
|
498
|
+
const emojis = {
|
|
499
|
+
goose: '🪿',
|
|
500
|
+
crush: '💘',
|
|
501
|
+
aider: '🛠',
|
|
502
|
+
qwen: '🐉',
|
|
503
|
+
pi: 'π',
|
|
504
|
+
openhands: '🤲',
|
|
505
|
+
amp: '⚡',
|
|
506
|
+
}
|
|
507
|
+
return emojis[toolMode] || '🧰'
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* 📖 Load backups from ~/.free-coding-models-backups.json
|
|
512
|
+
*/
|
|
513
|
+
function loadBackups() {
|
|
514
|
+
if (!existsSync(BACKUP_PATH)) {
|
|
515
|
+
return { disabledModels: [] }
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const content = readFileSync(BACKUP_PATH, 'utf8')
|
|
519
|
+
return JSON.parse(content)
|
|
520
|
+
} catch (err) {
|
|
521
|
+
return { disabledModels: [] }
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* 📖 Save backups to ~/.free-coding-models-backups.json
|
|
527
|
+
*/
|
|
528
|
+
function saveBackups(backups) {
|
|
529
|
+
const dir = dirname(BACKUP_PATH)
|
|
530
|
+
if (!existsSync(dir)) {
|
|
531
|
+
mkdirSync(dir, { recursive: true })
|
|
532
|
+
}
|
|
533
|
+
writeFileSync(BACKUP_PATH, JSON.stringify(backups, null, 2))
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* 📖 Soft-delete a model from tool config with backup
|
|
538
|
+
*/
|
|
539
|
+
export function softDeleteModel(toolMode, modelId, paths = getToolConfigPaths()) {
|
|
540
|
+
const configPath = paths[toolMode === 'pi' ? 'piSettings' : toolMode]
|
|
541
|
+
if (!existsSync(configPath)) {
|
|
542
|
+
return { success: false, error: 'Config file not found' }
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
let originalContent = readFileSync(configPath, 'utf8')
|
|
547
|
+
let newContent = originalContent
|
|
548
|
+
let modified = false
|
|
549
|
+
|
|
550
|
+
switch (toolMode) {
|
|
551
|
+
case 'goose':
|
|
552
|
+
if (originalContent.includes(`GOOSE_MODEL: ${modelId}`)) {
|
|
553
|
+
newContent = originalContent.replace(/^GOOSE_MODEL:.*$/m, '# GOOSE_MODEL: (disabled by FCM)\n# GOOSE_MODEL: ' + modelId)
|
|
554
|
+
modified = true
|
|
555
|
+
}
|
|
556
|
+
break
|
|
557
|
+
|
|
558
|
+
case 'crush':
|
|
559
|
+
const crushConfig = JSON.parse(originalContent)
|
|
560
|
+
if (crushConfig.models?.large?.model === modelId || crushConfig.models?.small?.model === modelId) {
|
|
561
|
+
if (crushConfig.models?.large?.model === modelId) {
|
|
562
|
+
delete crushConfig.models.large
|
|
563
|
+
}
|
|
564
|
+
if (crushConfig.models?.small?.model === modelId) {
|
|
565
|
+
delete crushConfig.models.small
|
|
566
|
+
}
|
|
567
|
+
newContent = JSON.stringify(crushConfig, null, 2)
|
|
568
|
+
modified = true
|
|
569
|
+
}
|
|
570
|
+
break
|
|
571
|
+
|
|
572
|
+
case 'aider':
|
|
573
|
+
if (originalContent.includes(`model: openai/${modelId}`)) {
|
|
574
|
+
newContent = originalContent.replace(/^model:.*$/m, '# model: (disabled by FCM)\n# model: openai/' + modelId)
|
|
575
|
+
modified = true
|
|
576
|
+
}
|
|
577
|
+
break
|
|
578
|
+
|
|
579
|
+
case 'qwen':
|
|
580
|
+
const qwenConfig = JSON.parse(originalContent)
|
|
581
|
+
if (qwenConfig.model === modelId) {
|
|
582
|
+
delete qwenConfig.model
|
|
583
|
+
newContent = JSON.stringify(qwenConfig, null, 2)
|
|
584
|
+
modified = true
|
|
585
|
+
}
|
|
586
|
+
break
|
|
587
|
+
|
|
588
|
+
case 'pi':
|
|
589
|
+
const piConfig = JSON.parse(originalContent)
|
|
590
|
+
if (piConfig.defaultModel === modelId) {
|
|
591
|
+
delete piConfig.defaultModel
|
|
592
|
+
newContent = JSON.stringify(piConfig, null, 2)
|
|
593
|
+
modified = true
|
|
594
|
+
}
|
|
595
|
+
break
|
|
596
|
+
|
|
597
|
+
case 'openhands':
|
|
598
|
+
if (originalContent.includes(`export LLM_MODEL="${modelId}"`) || originalContent.includes(`export LLM_MODEL='${modelId}'`)) {
|
|
599
|
+
newContent = originalContent.replace(/^export LLM_MODEL=.*$/m, '# export LLM_MODEL: (disabled by FCM)\n# export LLM_MODEL="' + modelId + '"')
|
|
600
|
+
modified = true
|
|
601
|
+
}
|
|
602
|
+
break
|
|
603
|
+
|
|
604
|
+
case 'amp':
|
|
605
|
+
const ampConfig = JSON.parse(originalContent)
|
|
606
|
+
if (ampConfig['amp.model'] === modelId) {
|
|
607
|
+
delete ampConfig['amp.model']
|
|
608
|
+
newContent = JSON.stringify(ampConfig, null, 2)
|
|
609
|
+
modified = true
|
|
610
|
+
}
|
|
611
|
+
break
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (!modified) {
|
|
615
|
+
return { success: false, error: 'Model not found in config' }
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
writeFileSync(configPath, newContent)
|
|
619
|
+
|
|
620
|
+
const backups = loadBackups()
|
|
621
|
+
backups.disabledModels.push({
|
|
622
|
+
id: `${toolMode}-${modelId}-${new Date().toISOString()}`,
|
|
623
|
+
toolMode,
|
|
624
|
+
modelId,
|
|
625
|
+
originalConfig: originalContent,
|
|
626
|
+
configPath,
|
|
627
|
+
disabledAt: new Date().toISOString(),
|
|
628
|
+
reason: 'user_deleted',
|
|
629
|
+
})
|
|
630
|
+
saveBackups(backups)
|
|
631
|
+
|
|
632
|
+
return { success: true }
|
|
633
|
+
} catch (err) {
|
|
634
|
+
return { success: false, error: err.message }
|
|
635
|
+
}
|
|
636
|
+
}
|