free-coding-models 0.3.11 → 0.3.13
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 +24 -0
- package/README.md +112 -1134
- package/bin/free-coding-models.js +18 -170
- package/package.json +2 -3
- package/src/cli-help.js +0 -18
- package/src/config.js +5 -117
- package/src/endpoint-installer.js +26 -64
- package/src/key-handler.js +90 -443
- 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 +28 -520
- package/src/product-flags.js +14 -0
- package/src/render-helpers.js +2 -34
- package/src/render-table.js +11 -19
- 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 +3 -68
- 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 -157
- package/src/log-reader.js +0 -195
- package/src/opencode-sync.js +0 -200
- package/src/proxy-foreground.js +0 -234
- package/src/proxy-server.js +0 -1506
- package/src/proxy-sync.js +0 -591
- 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
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/legacy-proxy-cleanup.js
|
|
3
|
+
* @description Best-effort cleanup for discontinued proxy-era config leftovers.
|
|
4
|
+
*
|
|
5
|
+
* @details
|
|
6
|
+
* 📖 The old global proxy/daemon stack has been removed from the product, but
|
|
7
|
+
* 📖 some users may still have persisted `fcm-proxy` entries, env files, or
|
|
8
|
+
* 📖 runtime artifacts from earlier versions.
|
|
9
|
+
*
|
|
10
|
+
* 📖 This module removes only the legacy proxy markers that are now obsolete:
|
|
11
|
+
* - `fcm-proxy` providers inside tool configs
|
|
12
|
+
* - proxy-only env files for removed tools
|
|
13
|
+
* - daemon/log artifacts from the old bridge
|
|
14
|
+
* - stale proxy fields in `~/.free-coding-models.json`
|
|
15
|
+
*
|
|
16
|
+
* 📖 It intentionally preserves current direct-provider installs such as
|
|
17
|
+
* 📖 `fcm-nvidia`, `fcm-groq`, or the current OpenHands env file when it is
|
|
18
|
+
* 📖 clearly configured for direct provider usage instead of the removed proxy.
|
|
19
|
+
*
|
|
20
|
+
* @functions
|
|
21
|
+
* → `cleanupLegacyProxyArtifacts` — remove discontinued proxy-era config files and entries
|
|
22
|
+
*
|
|
23
|
+
* @exports cleanupLegacyProxyArtifacts
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs'
|
|
27
|
+
import { homedir } from 'node:os'
|
|
28
|
+
import { join } from 'node:path'
|
|
29
|
+
|
|
30
|
+
const LEGACY_TOOL_MODES = new Set(['claude-code', 'codex', 'gemini'])
|
|
31
|
+
const LEGACY_RUNTIME_FILES = ['daemon.json', 'daemon-stdout.log', 'daemon-stderr.log', 'request-log.jsonl']
|
|
32
|
+
const LEGACY_ENV_FILES = ['.fcm-claude-code-env', '.fcm-codex-env', '.fcm-gemini-env']
|
|
33
|
+
|
|
34
|
+
function getDefaultPaths(homeDir) {
|
|
35
|
+
return {
|
|
36
|
+
configPath: join(homeDir, '.free-coding-models.json'),
|
|
37
|
+
dataDir: join(homeDir, '.free-coding-models'),
|
|
38
|
+
opencodeConfigPath: join(homeDir, '.config', 'opencode', 'opencode.json'),
|
|
39
|
+
openclawConfigPath: join(homeDir, '.openclaw', 'openclaw.json'),
|
|
40
|
+
crushConfigPath: join(homeDir, '.config', 'crush', 'crush.json'),
|
|
41
|
+
gooseProvidersDir: join(homeDir, '.config', 'goose', 'custom_providers'),
|
|
42
|
+
gooseSecretsPath: join(homeDir, '.config', 'goose', 'secrets.yaml'),
|
|
43
|
+
gooseConfigPath: join(homeDir, '.config', 'goose', 'config.yaml'),
|
|
44
|
+
piModelsPath: join(homeDir, '.pi', 'agent', 'models.json'),
|
|
45
|
+
piSettingsPath: join(homeDir, '.pi', 'agent', 'settings.json'),
|
|
46
|
+
aiderConfigPath: join(homeDir, '.aider.conf.yml'),
|
|
47
|
+
ampConfigPath: join(homeDir, '.config', 'amp', 'settings.json'),
|
|
48
|
+
qwenConfigPath: join(homeDir, '.qwen', 'settings.json'),
|
|
49
|
+
launchAgentPath: join(homeDir, 'Library', 'LaunchAgents', 'com.fcm.proxy.plist'),
|
|
50
|
+
systemdServicePath: join(homeDir, '.config', 'systemd', 'user', 'fcm-proxy.service'),
|
|
51
|
+
shellProfilePaths: [
|
|
52
|
+
join(homeDir, '.zshrc'),
|
|
53
|
+
join(homeDir, '.bashrc'),
|
|
54
|
+
join(homeDir, '.bash_profile'),
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createSummary() {
|
|
60
|
+
return {
|
|
61
|
+
changed: false,
|
|
62
|
+
removedFiles: [],
|
|
63
|
+
updatedFiles: [],
|
|
64
|
+
removedEntries: 0,
|
|
65
|
+
errors: [],
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function noteRemovedFile(summary, filePath) {
|
|
70
|
+
summary.changed = true
|
|
71
|
+
summary.removedFiles.push(filePath)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function noteUpdatedFile(summary, filePath, removedEntries = 1) {
|
|
75
|
+
summary.changed = true
|
|
76
|
+
summary.updatedFiles.push(filePath)
|
|
77
|
+
summary.removedEntries += removedEntries
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function noteError(summary, filePath, error) {
|
|
81
|
+
summary.errors.push(`${filePath}: ${error instanceof Error ? error.message : String(error)}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readJsonFile(filePath, fallback = null) {
|
|
85
|
+
if (!existsSync(filePath)) return fallback
|
|
86
|
+
return JSON.parse(readFileSync(filePath, 'utf8'))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function writeJsonFile(filePath, value) {
|
|
90
|
+
writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function deleteFileIfExists(filePath, summary) {
|
|
94
|
+
if (!existsSync(filePath)) return false
|
|
95
|
+
try {
|
|
96
|
+
unlinkSync(filePath)
|
|
97
|
+
noteRemovedFile(summary, filePath)
|
|
98
|
+
return true
|
|
99
|
+
} catch (error) {
|
|
100
|
+
noteError(summary, filePath, error)
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function updateJsonFile(filePath, mutate, summary) {
|
|
106
|
+
if (!existsSync(filePath)) return
|
|
107
|
+
try {
|
|
108
|
+
const data = readJsonFile(filePath, {})
|
|
109
|
+
const removedEntries = mutate(data)
|
|
110
|
+
if (removedEntries > 0) {
|
|
111
|
+
writeJsonFile(filePath, data)
|
|
112
|
+
noteUpdatedFile(summary, filePath, removedEntries)
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
noteError(summary, filePath, error)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function cleanupMainConfig(filePath, summary) {
|
|
120
|
+
if (!existsSync(filePath)) return
|
|
121
|
+
try {
|
|
122
|
+
const config = readJsonFile(filePath, {})
|
|
123
|
+
let removedEntries = 0
|
|
124
|
+
|
|
125
|
+
if (config.settings && typeof config.settings === 'object' && 'proxy' in config.settings) {
|
|
126
|
+
delete config.settings.proxy
|
|
127
|
+
removedEntries += 1
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if ('proxySettings' in config) {
|
|
131
|
+
delete config.proxySettings
|
|
132
|
+
removedEntries += 1
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (Array.isArray(config.endpointInstalls)) {
|
|
136
|
+
const before = config.endpointInstalls.length
|
|
137
|
+
config.endpointInstalls = config.endpointInstalls.filter(
|
|
138
|
+
(entry) => !LEGACY_TOOL_MODES.has(entry?.toolMode)
|
|
139
|
+
)
|
|
140
|
+
removedEntries += before - config.endpointInstalls.length
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (config.settings?.preferredToolMode && LEGACY_TOOL_MODES.has(config.settings.preferredToolMode)) {
|
|
144
|
+
config.settings.preferredToolMode = 'opencode'
|
|
145
|
+
removedEntries += 1
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (removedEntries > 0) {
|
|
149
|
+
writeJsonFile(filePath, config)
|
|
150
|
+
noteUpdatedFile(summary, filePath, removedEntries)
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
noteError(summary, filePath, error)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function cleanupRuntimeFiles(dataDir, summary) {
|
|
158
|
+
for (const fileName of LEGACY_RUNTIME_FILES) {
|
|
159
|
+
deleteFileIfExists(join(dataDir, fileName), summary)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function cleanupLegacyEnvFiles(homeDir, summary) {
|
|
164
|
+
for (const fileName of LEGACY_ENV_FILES) {
|
|
165
|
+
deleteFileIfExists(join(homeDir, fileName), summary)
|
|
166
|
+
deleteFileIfExists(join(homeDir, `${fileName}.bak`), summary)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function cleanupShellProfiles(profilePaths, summary) {
|
|
171
|
+
const patterns = [
|
|
172
|
+
/# 📖 FCM Proxy — Claude Code env vars/,
|
|
173
|
+
/\.fcm-claude-code-env/,
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
for (const profilePath of profilePaths) {
|
|
177
|
+
if (!existsSync(profilePath)) continue
|
|
178
|
+
try {
|
|
179
|
+
const raw = readFileSync(profilePath, 'utf8')
|
|
180
|
+
const lines = raw.split(/\r?\n/)
|
|
181
|
+
const filtered = lines.filter((line) => !patterns.some((pattern) => pattern.test(line)))
|
|
182
|
+
if (filtered.join('\n') !== lines.join('\n')) {
|
|
183
|
+
writeFileSync(profilePath, filtered.join('\n'))
|
|
184
|
+
noteUpdatedFile(summary, profilePath, 1)
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
noteError(summary, profilePath, error)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function cleanupOpenCode(filePath, summary) {
|
|
193
|
+
updateJsonFile(filePath, (config) => {
|
|
194
|
+
let removedEntries = 0
|
|
195
|
+
if (config.provider?.['fcm-proxy']) {
|
|
196
|
+
delete config.provider['fcm-proxy']
|
|
197
|
+
removedEntries += 1
|
|
198
|
+
if (Object.keys(config.provider).length === 0) delete config.provider
|
|
199
|
+
}
|
|
200
|
+
if (typeof config.model === 'string' && config.model.startsWith('fcm-proxy/')) {
|
|
201
|
+
delete config.model
|
|
202
|
+
removedEntries += 1
|
|
203
|
+
}
|
|
204
|
+
return removedEntries
|
|
205
|
+
}, summary)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function cleanupOpenClaw(filePath, summary) {
|
|
209
|
+
updateJsonFile(filePath, (config) => {
|
|
210
|
+
let removedEntries = 0
|
|
211
|
+
if (config.models?.providers?.['fcm-proxy']) {
|
|
212
|
+
delete config.models.providers['fcm-proxy']
|
|
213
|
+
removedEntries += 1
|
|
214
|
+
}
|
|
215
|
+
if (config.agents?.defaults?.models && typeof config.agents.defaults.models === 'object') {
|
|
216
|
+
for (const key of Object.keys(config.agents.defaults.models)) {
|
|
217
|
+
if (key.startsWith('fcm-proxy/')) {
|
|
218
|
+
delete config.agents.defaults.models[key]
|
|
219
|
+
removedEntries += 1
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return removedEntries
|
|
224
|
+
}, summary)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function cleanupCrush(filePath, summary) {
|
|
228
|
+
updateJsonFile(filePath, (config) => {
|
|
229
|
+
let removedEntries = 0
|
|
230
|
+
if (config.providers?.['fcm-proxy']) {
|
|
231
|
+
delete config.providers['fcm-proxy']
|
|
232
|
+
removedEntries += 1
|
|
233
|
+
}
|
|
234
|
+
if (config.models?.large?.provider === 'fcm-proxy') {
|
|
235
|
+
delete config.models.large
|
|
236
|
+
removedEntries += 1
|
|
237
|
+
}
|
|
238
|
+
if (config.models?.small?.provider === 'fcm-proxy') {
|
|
239
|
+
delete config.models.small
|
|
240
|
+
removedEntries += 1
|
|
241
|
+
}
|
|
242
|
+
return removedEntries
|
|
243
|
+
}, summary)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function readSimpleYamlMap(filePath) {
|
|
247
|
+
if (!existsSync(filePath)) return {}
|
|
248
|
+
const output = {}
|
|
249
|
+
const lines = readFileSync(filePath, 'utf8').split(/\r?\n/)
|
|
250
|
+
for (const line of lines) {
|
|
251
|
+
if (!line.trim() || line.trim().startsWith('#')) continue
|
|
252
|
+
const match = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/)
|
|
253
|
+
if (!match) continue
|
|
254
|
+
output[match[1]] = match[2].trim().replace(/^['"]|['"]$/g, '')
|
|
255
|
+
}
|
|
256
|
+
return output
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function writeSimpleYamlMap(filePath, entries) {
|
|
260
|
+
const lines = Object.keys(entries)
|
|
261
|
+
.sort()
|
|
262
|
+
.map((key) => `${key}: ${JSON.stringify(String(entries[key] ?? ''))}`)
|
|
263
|
+
writeFileSync(filePath, lines.join('\n') + '\n')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function cleanupGoose(paths, summary) {
|
|
267
|
+
deleteFileIfExists(join(paths.gooseProvidersDir, 'fcm-proxy.json'), summary)
|
|
268
|
+
|
|
269
|
+
if (existsSync(paths.gooseSecretsPath)) {
|
|
270
|
+
try {
|
|
271
|
+
const secrets = readSimpleYamlMap(paths.gooseSecretsPath)
|
|
272
|
+
if ('FCM_PROXY_API_KEY' in secrets) {
|
|
273
|
+
delete secrets.FCM_PROXY_API_KEY
|
|
274
|
+
writeSimpleYamlMap(paths.gooseSecretsPath, secrets)
|
|
275
|
+
noteUpdatedFile(summary, paths.gooseSecretsPath, 1)
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
noteError(summary, paths.gooseSecretsPath, error)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (existsSync(paths.gooseConfigPath)) {
|
|
283
|
+
try {
|
|
284
|
+
const lines = readFileSync(paths.gooseConfigPath, 'utf8').split(/\r?\n/)
|
|
285
|
+
const hadLegacyProvider = lines.some((line) => /^GOOSE_PROVIDER:\s*fcm-proxy\s*$/.test(line))
|
|
286
|
+
if (hadLegacyProvider) {
|
|
287
|
+
const filtered = lines.filter((line) => {
|
|
288
|
+
if (/^GOOSE_PROVIDER:\s*fcm-proxy\s*$/.test(line)) return false
|
|
289
|
+
if (/^GOOSE_MODEL:\s*/.test(line)) return false
|
|
290
|
+
return true
|
|
291
|
+
})
|
|
292
|
+
writeFileSync(paths.gooseConfigPath, filtered.join('\n'))
|
|
293
|
+
noteUpdatedFile(summary, paths.gooseConfigPath, 2)
|
|
294
|
+
}
|
|
295
|
+
} catch (error) {
|
|
296
|
+
noteError(summary, paths.gooseConfigPath, error)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function cleanupPi(paths, summary) {
|
|
302
|
+
updateJsonFile(paths.piModelsPath, (config) => {
|
|
303
|
+
let removedEntries = 0
|
|
304
|
+
if (config.providers?.['fcm-proxy']) {
|
|
305
|
+
delete config.providers['fcm-proxy']
|
|
306
|
+
removedEntries += 1
|
|
307
|
+
}
|
|
308
|
+
return removedEntries
|
|
309
|
+
}, summary)
|
|
310
|
+
|
|
311
|
+
updateJsonFile(paths.piSettingsPath, (config) => {
|
|
312
|
+
let removedEntries = 0
|
|
313
|
+
if (config.defaultProvider === 'fcm-proxy') {
|
|
314
|
+
delete config.defaultProvider
|
|
315
|
+
removedEntries += 1
|
|
316
|
+
}
|
|
317
|
+
if (typeof config.defaultModel === 'string' && config.defaultModel.startsWith('fcm-proxy/')) {
|
|
318
|
+
delete config.defaultModel
|
|
319
|
+
removedEntries += 1
|
|
320
|
+
}
|
|
321
|
+
return removedEntries
|
|
322
|
+
}, summary)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function cleanupAider(filePath, summary) {
|
|
326
|
+
if (!existsSync(filePath)) return
|
|
327
|
+
try {
|
|
328
|
+
const content = readFileSync(filePath, 'utf8')
|
|
329
|
+
const isLegacyProxyConfig = content.includes('FCM Proxy V2') || /openai-api-base:\s*http:\/\/127\.0\.0\.1:/i.test(content)
|
|
330
|
+
if (isLegacyProxyConfig) {
|
|
331
|
+
unlinkSync(filePath)
|
|
332
|
+
noteRemovedFile(summary, filePath)
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
noteError(summary, filePath, error)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function cleanupAmp(filePath, summary) {
|
|
340
|
+
updateJsonFile(filePath, (config) => {
|
|
341
|
+
let removedEntries = 0
|
|
342
|
+
const usesLegacyLocalhost = typeof config['amp.url'] === 'string' && /127\.0\.0\.1|localhost/.test(config['amp.url'])
|
|
343
|
+
if (usesLegacyLocalhost) {
|
|
344
|
+
delete config['amp.url']
|
|
345
|
+
removedEntries += 1
|
|
346
|
+
if (typeof config['amp.model'] === 'string') {
|
|
347
|
+
delete config['amp.model']
|
|
348
|
+
removedEntries += 1
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return removedEntries
|
|
352
|
+
}, summary)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function cleanupQwen(filePath, summary) {
|
|
356
|
+
updateJsonFile(filePath, (config) => {
|
|
357
|
+
let removedEntries = 0
|
|
358
|
+
const removedIds = new Set()
|
|
359
|
+
if (Array.isArray(config.modelProviders?.openai)) {
|
|
360
|
+
const next = []
|
|
361
|
+
for (const entry of config.modelProviders.openai) {
|
|
362
|
+
const isLegacyEntry = entry?.envKey === 'FCM_PROXY_API_KEY'
|
|
363
|
+
|| (typeof entry?.baseUrl === 'string' && /127\.0\.0\.1|localhost/.test(entry.baseUrl))
|
|
364
|
+
|| (typeof entry?.id === 'string' && entry.id.startsWith('fcm-proxy/'))
|
|
365
|
+
if (isLegacyEntry) {
|
|
366
|
+
if (typeof entry?.id === 'string') removedIds.add(entry.id)
|
|
367
|
+
removedEntries += 1
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
next.push(entry)
|
|
371
|
+
}
|
|
372
|
+
config.modelProviders.openai = next
|
|
373
|
+
}
|
|
374
|
+
if (typeof config.model === 'string' && (config.model.startsWith('fcm-proxy/') || removedIds.has(config.model))) {
|
|
375
|
+
delete config.model
|
|
376
|
+
removedEntries += 1
|
|
377
|
+
}
|
|
378
|
+
return removedEntries
|
|
379
|
+
}, summary)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function cleanupOpenHandsEnv(homeDir, summary) {
|
|
383
|
+
const filePath = join(homeDir, '.fcm-openhands-env')
|
|
384
|
+
if (!existsSync(filePath)) return
|
|
385
|
+
try {
|
|
386
|
+
const content = readFileSync(filePath, 'utf8')
|
|
387
|
+
const isLegacyProxyEnv = content.includes('FCM Proxy V2') || /127\.0\.0\.1|localhost/.test(content)
|
|
388
|
+
if (isLegacyProxyEnv) {
|
|
389
|
+
unlinkSync(filePath)
|
|
390
|
+
noteRemovedFile(summary, filePath)
|
|
391
|
+
}
|
|
392
|
+
} catch (error) {
|
|
393
|
+
noteError(summary, filePath, error)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 📖 cleanupLegacyProxyArtifacts removes stale proxy-era artifacts from older
|
|
399
|
+
* 📖 releases. It is safe to run multiple times.
|
|
400
|
+
*
|
|
401
|
+
* @param {{
|
|
402
|
+
* homeDir?: string,
|
|
403
|
+
* paths?: Partial<ReturnType<typeof getDefaultPaths>>
|
|
404
|
+
* }} [options]
|
|
405
|
+
* @returns {{ changed: boolean, removedFiles: string[], updatedFiles: string[], removedEntries: number, errors: string[] }}
|
|
406
|
+
*/
|
|
407
|
+
export function cleanupLegacyProxyArtifacts(options = {}) {
|
|
408
|
+
const homeDir = options.homeDir || homedir()
|
|
409
|
+
const paths = {
|
|
410
|
+
...getDefaultPaths(homeDir),
|
|
411
|
+
...(options.paths || {}),
|
|
412
|
+
}
|
|
413
|
+
const summary = createSummary()
|
|
414
|
+
|
|
415
|
+
cleanupMainConfig(paths.configPath, summary)
|
|
416
|
+
cleanupRuntimeFiles(paths.dataDir, summary)
|
|
417
|
+
cleanupLegacyEnvFiles(homeDir, summary)
|
|
418
|
+
cleanupShellProfiles(paths.shellProfilePaths || [], summary)
|
|
419
|
+
cleanupOpenCode(paths.opencodeConfigPath, summary)
|
|
420
|
+
cleanupOpenClaw(paths.openclawConfigPath, summary)
|
|
421
|
+
cleanupCrush(paths.crushConfigPath, summary)
|
|
422
|
+
cleanupGoose(paths, summary)
|
|
423
|
+
cleanupPi(paths, summary)
|
|
424
|
+
cleanupAider(paths.aiderConfigPath, summary)
|
|
425
|
+
cleanupAmp(paths.ampConfigPath, summary)
|
|
426
|
+
cleanupQwen(paths.qwenConfigPath, summary)
|
|
427
|
+
cleanupOpenHandsEnv(homeDir, summary)
|
|
428
|
+
deleteFileIfExists(paths.launchAgentPath, summary)
|
|
429
|
+
deleteFileIfExists(paths.systemdServicePath, summary)
|
|
430
|
+
|
|
431
|
+
return summary
|
|
432
|
+
}
|
package/src/openclaw.js
CHANGED
|
@@ -1,136 +1,97 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file openclaw.js
|
|
3
|
-
* @description OpenClaw config helpers for
|
|
2
|
+
* @file src/openclaw.js
|
|
3
|
+
* @description OpenClaw config helpers for persisting the selected provider/model as the default.
|
|
4
4
|
*
|
|
5
5
|
* @details
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* - Set the selected model as the default primary model
|
|
6
|
+
* 📖 OpenClaw is config-driven: FCM does not launch a separate foreground CLI here.
|
|
7
|
+
* 📖 Pressing Enter in `OpenClaw` mode must therefore do two things reliably:
|
|
8
|
+
* - install the selected provider/model into `~/.openclaw/openclaw.json`
|
|
9
|
+
* - set that exact model as the default primary model for the next OpenClaw session
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* 📖 The old implementation was hard-coded to `nvidia/*`, which meant selecting
|
|
12
|
+
* 📖 a Groq/Cerebras/etc. row silently wrote the wrong provider/model into the
|
|
13
|
+
* 📖 OpenClaw config. This module now delegates to the shared direct-install
|
|
14
|
+
* 📖 writer so every supported provider uses the same contract.
|
|
15
|
+
*
|
|
16
|
+
* @functions
|
|
17
|
+
* → `loadOpenClawConfig` — read the OpenClaw config from disk
|
|
18
|
+
* → `saveOpenClawConfig` — persist the OpenClaw config to disk
|
|
19
|
+
* → `startOpenClaw` — install the selected provider/model and set it as default
|
|
16
20
|
*
|
|
17
21
|
* @exports { loadOpenClawConfig, saveOpenClawConfig, startOpenClaw }
|
|
18
|
-
*
|
|
22
|
+
*
|
|
23
|
+
* @see src/endpoint-installer.js
|
|
19
24
|
*/
|
|
20
25
|
|
|
21
26
|
import chalk from 'chalk'
|
|
22
|
-
import {
|
|
27
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
23
28
|
import { homedir } from 'os'
|
|
24
|
-
import { join } from 'path'
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
29
|
+
import { dirname, join } from 'path'
|
|
30
|
+
import { installProviderEndpoints } from './endpoint-installer.js'
|
|
31
|
+
import { ENV_VAR_NAMES } from './provider-metadata.js'
|
|
27
32
|
import { PROVIDER_COLOR } from './render-table.js'
|
|
28
33
|
|
|
29
|
-
// 📖 OpenClaw config: ~/.openclaw/openclaw.json (JSON format, may be JSON5 in newer versions)
|
|
30
34
|
const OPENCLAW_CONFIG = join(homedir(), '.openclaw', 'openclaw.json')
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
function getOpenClawConfigPath(options = {}) {
|
|
37
|
+
return options.paths?.openclawConfigPath || OPENCLAW_CONFIG
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function loadOpenClawConfig(options = {}) {
|
|
41
|
+
const filePath = getOpenClawConfigPath(options)
|
|
42
|
+
if (!existsSync(filePath)) return {}
|
|
34
43
|
try {
|
|
35
|
-
|
|
36
|
-
return JSON.parse(readFileSync(OPENCLAW_CONFIG, 'utf8'))
|
|
44
|
+
return JSON.parse(readFileSync(filePath, 'utf8'))
|
|
37
45
|
} catch {
|
|
38
46
|
return {}
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
export function saveOpenClawConfig(config) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
writeFileSync(OPENCLAW_CONFIG, JSON.stringify(config, null, 2))
|
|
50
|
+
export function saveOpenClawConfig(config, options = {}) {
|
|
51
|
+
const filePath = getOpenClawConfigPath(options)
|
|
52
|
+
mkdirSync(dirname(filePath), { recursive: true })
|
|
53
|
+
writeFileSync(filePath, JSON.stringify(config, null, 2))
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
/**
|
|
57
|
+
* 📖 startOpenClaw installs the selected provider/model into OpenClaw and sets
|
|
58
|
+
* 📖 it as the primary default model. OpenClaw itself is not launched here.
|
|
59
|
+
*
|
|
60
|
+
* @param {{ providerKey: string, modelId: string, label: string }} model
|
|
61
|
+
* @param {Record<string, unknown>} config
|
|
62
|
+
* @param {{ paths?: { openclawConfigPath?: string } }} [options]
|
|
63
|
+
* @returns {Promise<ReturnType<typeof installProviderEndpoints> | null>}
|
|
64
|
+
*/
|
|
65
|
+
export async function startOpenClaw(model, config, options = {}) {
|
|
66
|
+
const providerRgb = PROVIDER_COLOR[model.providerKey] ?? [105, 190, 245]
|
|
67
|
+
const coloredProviderName = chalk.bold.rgb(...providerRgb)(model.providerKey)
|
|
54
68
|
console.log(chalk.rgb(255, 100, 50)(` 🦞 Setting ${chalk.bold(model.label)} as OpenClaw default…`))
|
|
55
|
-
console.log(chalk.dim(`
|
|
69
|
+
console.log(chalk.dim(` Provider: ${coloredProviderName}`))
|
|
70
|
+
console.log(chalk.dim(` Model: ${model.providerKey}/${model.modelId}`))
|
|
56
71
|
console.log()
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 📖 Patch models.json to add all NVIDIA models (fixes "not allowed" errors)
|
|
68
|
-
const patchResult = patchOpenClawModelsJson()
|
|
69
|
-
if (patchResult.wasPatched) {
|
|
70
|
-
console.log(chalk.dim(` ✨ Added ${patchResult.added} NVIDIA models to allowlist (${patchResult.total} total)`))
|
|
71
|
-
if (patchResult.backup) {
|
|
72
|
-
console.log(chalk.dim(` 💾 models.json backup: ${patchResult.backup}`))
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// 📖 Ensure models.providers section exists with nvidia NIM block.
|
|
77
|
-
// 📖 Per OpenClaw docs (docs.openclaw.ai/providers/nvidia), providers MUST be nested under
|
|
78
|
-
// 📖 "models.providers", NOT at the config root. Root-level "providers" is ignored by OpenClaw.
|
|
79
|
-
// 📖 API key is NOT stored in the provider block — it's read from env var NVIDIA_API_KEY.
|
|
80
|
-
// 📖 If needed, it can be stored under the root "env" key: { env: { NVIDIA_API_KEY: "nvapi-..." } }
|
|
81
|
-
if (!config.models) config.models = {}
|
|
82
|
-
if (!config.models.providers) config.models.providers = {}
|
|
83
|
-
if (!config.models.providers.nvidia) {
|
|
84
|
-
config.models.providers.nvidia = {
|
|
85
|
-
baseUrl: 'https://integrate.api.nvidia.com/v1',
|
|
86
|
-
api: 'openai-completions',
|
|
87
|
-
models: [],
|
|
88
|
-
}
|
|
89
|
-
// 📖 Color provider name the same way as in the main table
|
|
90
|
-
const providerRgb = PROVIDER_COLOR['nvidia'] ?? [105, 190, 245]
|
|
91
|
-
const coloredProviderName = chalk.bold.rgb(...providerRgb)('nvidia')
|
|
92
|
-
console.log(chalk.dim(` ➕ Added ${coloredProviderName} provider block to OpenClaw config (models.providers.nvidia)`))
|
|
93
|
-
}
|
|
94
|
-
// 📖 Ensure models array exists even if the provider block was created by an older version
|
|
95
|
-
if (!Array.isArray(config.models.providers.nvidia.models)) {
|
|
96
|
-
config.models.providers.nvidia.models = []
|
|
97
|
-
}
|
|
73
|
+
try {
|
|
74
|
+
const result = installProviderEndpoints(config, model.providerKey, 'openclaw', {
|
|
75
|
+
scope: 'selected',
|
|
76
|
+
modelIds: [model.modelId],
|
|
77
|
+
track: false,
|
|
78
|
+
paths: options.paths,
|
|
79
|
+
})
|
|
98
80
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
81
|
+
const providerEnvName = ENV_VAR_NAMES[model.providerKey]
|
|
82
|
+
console.log(chalk.rgb(255, 140, 0)(` ✓ Default model set to: ${result.primaryModelRef || `${result.providerId}/${model.modelId}`}`))
|
|
83
|
+
console.log()
|
|
84
|
+
console.log(chalk.dim(` 📄 Config updated: ${result.path}`))
|
|
85
|
+
if (result.backupPath) console.log(chalk.dim(` 💾 Backup: ${result.backupPath}`))
|
|
86
|
+
if (providerEnvName) console.log(chalk.dim(` 🔑 API key synced under config env.${providerEnvName}`))
|
|
87
|
+
console.log()
|
|
88
|
+
console.log(chalk.dim(' 💡 OpenClaw will reload config automatically when it notices the file change.'))
|
|
89
|
+
console.log(chalk.dim(` To apply manually: openclaw models set ${result.primaryModelRef || `${result.providerId}/${model.modelId}`}`))
|
|
90
|
+
console.log()
|
|
91
|
+
return result
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.log(chalk.red(` X Could not configure OpenClaw: ${error instanceof Error ? error.message : String(error)}`))
|
|
94
|
+
console.log()
|
|
95
|
+
return null
|
|
108
96
|
}
|
|
109
|
-
|
|
110
|
-
// 📖 Set as the default primary model for all agents.
|
|
111
|
-
// 📖 Format: "provider/model-id" — e.g. "nvidia/deepseek-ai/deepseek-v3.2"
|
|
112
|
-
if (!config.agents) config.agents = {}
|
|
113
|
-
if (!config.agents.defaults) config.agents.defaults = {}
|
|
114
|
-
if (!config.agents.defaults.model) config.agents.defaults.model = {}
|
|
115
|
-
config.agents.defaults.model.primary = `nvidia/${model.modelId}`
|
|
116
|
-
|
|
117
|
-
// 📖 REQUIRED: OpenClaw requires the model to be explicitly listed in agents.defaults.models
|
|
118
|
-
// 📖 (the allowlist). Without this entry, OpenClaw rejects the model with "not allowed".
|
|
119
|
-
// 📖 See: https://docs.openclaw.ai/gateway/configuration-reference
|
|
120
|
-
if (!config.agents.defaults.models) config.agents.defaults.models = {}
|
|
121
|
-
config.agents.defaults.models[`nvidia/${model.modelId}`] = {}
|
|
122
|
-
|
|
123
|
-
saveOpenClawConfig(config)
|
|
124
|
-
|
|
125
|
-
console.log(chalk.rgb(255, 140, 0)(` ✓ Default model set to: nvidia/${model.modelId}`))
|
|
126
|
-
console.log()
|
|
127
|
-
console.log(chalk.dim(' 📄 Config updated: ' + OPENCLAW_CONFIG))
|
|
128
|
-
console.log()
|
|
129
|
-
// 📖 "openclaw restart" does NOT exist. The gateway auto-reloads on config file changes.
|
|
130
|
-
// 📖 To apply manually: use "openclaw models set" or "openclaw configure"
|
|
131
|
-
// 📖 See: https://docs.openclaw.ai/gateway/configuration
|
|
132
|
-
console.log(chalk.dim(' 💡 OpenClaw will reload config automatically (gateway.reload.mode).'))
|
|
133
|
-
console.log(chalk.dim(' To apply manually: openclaw models set nvidia/' + model.modelId))
|
|
134
|
-
console.log(chalk.dim(' Or run the setup wizard: openclaw configure'))
|
|
135
|
-
console.log()
|
|
136
97
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/opencode-config.js
|
|
3
|
+
* @description Small filesystem helpers for the shared OpenCode config file.
|
|
4
|
+
*
|
|
5
|
+
* @details
|
|
6
|
+
* 📖 The app still needs a stable way to read and write `opencode.json`
|
|
7
|
+
* 📖 for direct OpenCode CLI and Desktop launches.
|
|
8
|
+
* 📖 This module deliberately stays tiny so OpenCode launch code is not
|
|
9
|
+
* 📖 coupled to old bridge-specific sync behavior anymore.
|
|
10
|
+
*
|
|
11
|
+
* @functions
|
|
12
|
+
* → `loadOpenCodeConfig` — read `~/.config/opencode/opencode.json` safely
|
|
13
|
+
* → `saveOpenCodeConfig` — write `opencode.json` with a simple backup
|
|
14
|
+
* → `restoreOpenCodeBackup` — restore the last `.bak` copy if needed
|
|
15
|
+
*
|
|
16
|
+
* @exports loadOpenCodeConfig, saveOpenCodeConfig, restoreOpenCodeBackup
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
20
|
+
import { join } from 'node:path'
|
|
21
|
+
import { homedir } from 'node:os'
|
|
22
|
+
|
|
23
|
+
const OPENCODE_CONFIG_DIR = join(homedir(), '.config', 'opencode')
|
|
24
|
+
const OPENCODE_CONFIG_PATH = join(OPENCODE_CONFIG_DIR, 'opencode.json')
|
|
25
|
+
const OPENCODE_BACKUP_PATH = join(OPENCODE_CONFIG_DIR, 'opencode.json.bak')
|
|
26
|
+
|
|
27
|
+
export function loadOpenCodeConfig() {
|
|
28
|
+
try {
|
|
29
|
+
if (existsSync(OPENCODE_CONFIG_PATH)) {
|
|
30
|
+
return JSON.parse(readFileSync(OPENCODE_CONFIG_PATH, 'utf8'))
|
|
31
|
+
}
|
|
32
|
+
} catch {}
|
|
33
|
+
return {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function saveOpenCodeConfig(config) {
|
|
37
|
+
mkdirSync(OPENCODE_CONFIG_DIR, { recursive: true })
|
|
38
|
+
if (existsSync(OPENCODE_CONFIG_PATH)) {
|
|
39
|
+
copyFileSync(OPENCODE_CONFIG_PATH, OPENCODE_BACKUP_PATH)
|
|
40
|
+
}
|
|
41
|
+
writeFileSync(OPENCODE_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function restoreOpenCodeBackup() {
|
|
45
|
+
if (!existsSync(OPENCODE_BACKUP_PATH)) return false
|
|
46
|
+
copyFileSync(OPENCODE_BACKUP_PATH, OPENCODE_CONFIG_PATH)
|
|
47
|
+
return true
|
|
48
|
+
}
|