free-coding-models 0.3.0 → 0.3.1
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 +18 -0
- package/README.md +30 -20
- package/bin/free-coding-models.js +50 -19
- package/package.json +1 -1
- package/src/anthropic-translator.js +78 -8
- package/src/cli-help.js +108 -0
- package/src/config.js +2 -1
- package/src/endpoint-installer.js +5 -4
- package/src/key-handler.js +31 -34
- package/src/opencode.js +17 -12
- package/src/overlays.js +40 -53
- package/src/proxy-server.js +258 -4
- package/src/proxy-sync.js +16 -4
- package/src/render-helpers.js +4 -2
- package/src/render-table.js +34 -36
- package/src/responses-translator.js +423 -0
- package/src/tool-launchers.js +216 -19
- package/src/utils.js +31 -8
package/src/tool-launchers.js
CHANGED
|
@@ -19,15 +19,19 @@
|
|
|
19
19
|
* 📖 Crush: writes crush.json with provider config + models.large/small defaults
|
|
20
20
|
* 📖 Pi: uses --provider/--model CLI flags for guaranteed auto-selection
|
|
21
21
|
* 📖 Aider: writes ~/.aider.conf.yml + passes --model flag
|
|
22
|
-
* 📖 Claude Code: uses ANTHROPIC_BASE_URL
|
|
22
|
+
* 📖 Claude Code: uses ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN only (mirrors free-claude-code)
|
|
23
|
+
* 📖 Codex CLI: uses a custom model_provider override so Codex stays in explicit API-provider mode
|
|
24
|
+
* 📖 Gemini CLI: proxy mode is capability-gated because older builds do not support custom base URL routing cleanly
|
|
23
25
|
*
|
|
24
26
|
* @functions
|
|
25
27
|
* → `resolveLauncherModelId` — choose the provider-specific id or proxy slug for a launch
|
|
28
|
+
* → `buildCodexProxyArgs` — force Codex into a proxy-backed custom provider config
|
|
29
|
+
* → `inspectGeminiCliSupport` — detect whether the installed Gemini CLI can use proxy mode safely
|
|
26
30
|
* → `writeGooseConfig` — install provider + set GOOSE_PROVIDER/GOOSE_MODEL in config.yaml
|
|
27
31
|
* → `writeCrushConfig` — write provider + models.large/small to crush.json
|
|
28
32
|
* → `startExternalTool` — configure and launch the selected external tool mode
|
|
29
33
|
*
|
|
30
|
-
* @exports resolveLauncherModelId, startExternalTool
|
|
34
|
+
* @exports resolveLauncherModelId, buildCodexProxyArgs, inspectGeminiCliSupport, startExternalTool
|
|
31
35
|
*
|
|
32
36
|
* @see src/tool-metadata.js
|
|
33
37
|
* @see src/provider-metadata.js
|
|
@@ -35,10 +39,10 @@
|
|
|
35
39
|
*/
|
|
36
40
|
|
|
37
41
|
import chalk from 'chalk'
|
|
38
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'fs'
|
|
42
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, copyFileSync } from 'fs'
|
|
39
43
|
import { homedir } from 'os'
|
|
40
|
-
import { dirname, join } from 'path'
|
|
41
|
-
import { spawn } from 'child_process'
|
|
44
|
+
import { delimiter, dirname, join } from 'path'
|
|
45
|
+
import { spawn, spawnSync } from 'child_process'
|
|
42
46
|
import { sources } from '../sources.js'
|
|
43
47
|
import { PROVIDER_COLOR } from './render-table.js'
|
|
44
48
|
import { getApiKey, getProxySettings } from './config.js'
|
|
@@ -47,6 +51,31 @@ import { getToolMeta } from './tool-metadata.js'
|
|
|
47
51
|
import { ensureProxyRunning, resolveProxyModelId } from './opencode.js'
|
|
48
52
|
import { PROVIDER_METADATA } from './provider-metadata.js'
|
|
49
53
|
|
|
54
|
+
const OPENAI_COMPAT_ENV_KEYS = [
|
|
55
|
+
'OPENAI_API_KEY',
|
|
56
|
+
'OPENAI_BASE_URL',
|
|
57
|
+
'OPENAI_API_BASE',
|
|
58
|
+
'OPENAI_MODEL',
|
|
59
|
+
'LLM_API_KEY',
|
|
60
|
+
'LLM_BASE_URL',
|
|
61
|
+
'LLM_MODEL',
|
|
62
|
+
]
|
|
63
|
+
const ANTHROPIC_ENV_KEYS = [
|
|
64
|
+
'ANTHROPIC_API_KEY',
|
|
65
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
66
|
+
'ANTHROPIC_BASE_URL',
|
|
67
|
+
'ANTHROPIC_MODEL',
|
|
68
|
+
]
|
|
69
|
+
const GEMINI_ENV_KEYS = [
|
|
70
|
+
'GEMINI_API_KEY',
|
|
71
|
+
'GOOGLE_API_KEY',
|
|
72
|
+
'GOOGLE_GEMINI_BASE_URL',
|
|
73
|
+
'GOOGLE_VERTEX_BASE_URL',
|
|
74
|
+
]
|
|
75
|
+
const PROXY_SANITIZED_ENV_KEYS = [...OPENAI_COMPAT_ENV_KEYS, ...ANTHROPIC_ENV_KEYS, ...GEMINI_ENV_KEYS]
|
|
76
|
+
const GEMINI_PROXY_MIN_VERSION = '0.34.0'
|
|
77
|
+
const EXPERIMENTAL_PROXY_TOOLS_NOTE = 'FCM Proxy V2 support for external tools is still in beta, so some launch and authentication flows can remain flaky while the integration stabilizes.'
|
|
78
|
+
|
|
50
79
|
function ensureDir(filePath) {
|
|
51
80
|
const dir = dirname(filePath)
|
|
52
81
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
@@ -82,6 +111,16 @@ function getProviderBaseUrl(providerKey) {
|
|
|
82
111
|
.replace(/\/predictions$/i, '')
|
|
83
112
|
}
|
|
84
113
|
|
|
114
|
+
function deleteEnvKeys(env, keys) {
|
|
115
|
+
for (const key of keys) delete env[key]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function cloneInheritedEnv(inheritedEnv = process.env, sanitizeKeys = []) {
|
|
119
|
+
const env = { ...inheritedEnv }
|
|
120
|
+
deleteEnvKeys(env, sanitizeKeys)
|
|
121
|
+
return env
|
|
122
|
+
}
|
|
123
|
+
|
|
85
124
|
function applyOpenAiCompatEnv(env, apiKey, baseUrl, modelId) {
|
|
86
125
|
if (!apiKey || !baseUrl || !modelId) return env
|
|
87
126
|
env.OPENAI_API_KEY = apiKey
|
|
@@ -107,17 +146,23 @@ export function resolveLauncherModelId(model, useProxy = false) {
|
|
|
107
146
|
return model?.modelId ?? ''
|
|
108
147
|
}
|
|
109
148
|
|
|
110
|
-
function buildToolEnv(mode, model, config) {
|
|
149
|
+
export function buildToolEnv(mode, model, config, options = {}) {
|
|
150
|
+
const {
|
|
151
|
+
sanitize = false,
|
|
152
|
+
includeCompatDefaults = true,
|
|
153
|
+
includeProviderEnv = true,
|
|
154
|
+
inheritedEnv = process.env,
|
|
155
|
+
} = options
|
|
111
156
|
const providerKey = model.providerKey
|
|
112
157
|
const providerUrl = sources[providerKey]?.url || ''
|
|
113
158
|
const baseUrl = getProviderBaseUrl(providerKey)
|
|
114
159
|
const apiKey = getApiKey(config, providerKey)
|
|
115
|
-
const env =
|
|
160
|
+
const env = cloneInheritedEnv(inheritedEnv, sanitize ? PROXY_SANITIZED_ENV_KEYS : [])
|
|
116
161
|
const providerEnvName = ENV_VAR_NAMES[providerKey]
|
|
117
|
-
if (providerEnvName && apiKey) env[providerEnvName] = apiKey
|
|
162
|
+
if (includeProviderEnv && providerEnvName && apiKey) env[providerEnvName] = apiKey
|
|
118
163
|
|
|
119
164
|
// 📖 OpenAI-compatible defaults reused by multiple CLIs.
|
|
120
|
-
if (apiKey && baseUrl) {
|
|
165
|
+
if (includeCompatDefaults && apiKey && baseUrl) {
|
|
121
166
|
env.OPENAI_API_KEY = apiKey
|
|
122
167
|
env.OPENAI_BASE_URL = baseUrl
|
|
123
168
|
env.OPENAI_API_BASE = baseUrl
|
|
@@ -135,6 +180,7 @@ function buildToolEnv(mode, model, config) {
|
|
|
135
180
|
}
|
|
136
181
|
|
|
137
182
|
if (mode === 'gemini' && apiKey && baseUrl) {
|
|
183
|
+
env.GEMINI_API_KEY = apiKey
|
|
138
184
|
env.GOOGLE_API_KEY = apiKey
|
|
139
185
|
env.GOOGLE_GEMINI_BASE_URL = baseUrl
|
|
140
186
|
}
|
|
@@ -142,6 +188,114 @@ function buildToolEnv(mode, model, config) {
|
|
|
142
188
|
return { env, apiKey, baseUrl, providerUrl }
|
|
143
189
|
}
|
|
144
190
|
|
|
191
|
+
export function buildCodexProxyArgs(baseUrl) {
|
|
192
|
+
return [
|
|
193
|
+
'-c', 'model_provider="fcm_proxy"',
|
|
194
|
+
'-c', 'model_providers.fcm_proxy.name="FCM Proxy V2"',
|
|
195
|
+
'-c', `model_providers.fcm_proxy.base_url=${JSON.stringify(baseUrl)}`,
|
|
196
|
+
'-c', 'model_providers.fcm_proxy.env_key="FCM_PROXY_API_KEY"',
|
|
197
|
+
'-c', 'model_providers.fcm_proxy.wire_api="responses"',
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function compareSemver(a, b) {
|
|
202
|
+
const left = String(a || '').split('.').map(part => Number.parseInt(part, 10) || 0)
|
|
203
|
+
const right = String(b || '').split('.').map(part => Number.parseInt(part, 10) || 0)
|
|
204
|
+
const length = Math.max(left.length, right.length)
|
|
205
|
+
for (let idx = 0; idx < length; idx++) {
|
|
206
|
+
const lhs = left[idx] || 0
|
|
207
|
+
const rhs = right[idx] || 0
|
|
208
|
+
if (lhs > rhs) return 1
|
|
209
|
+
if (lhs < rhs) return -1
|
|
210
|
+
}
|
|
211
|
+
return 0
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function findExecutableOnPath(command) {
|
|
215
|
+
const pathValue = process.env.PATH || ''
|
|
216
|
+
const candidates = process.platform === 'win32'
|
|
217
|
+
? [command, `${command}.cmd`, `${command}.exe`]
|
|
218
|
+
: [command]
|
|
219
|
+
|
|
220
|
+
for (const dir of pathValue.split(delimiter).filter(Boolean)) {
|
|
221
|
+
for (const candidate of candidates) {
|
|
222
|
+
const fullPath = join(dir, candidate)
|
|
223
|
+
try {
|
|
224
|
+
accessSync(fullPath, constants.X_OK)
|
|
225
|
+
return fullPath
|
|
226
|
+
} catch { /* not executable */ }
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function findPackageJsonUpwards(startPath) {
|
|
233
|
+
let current = dirname(startPath)
|
|
234
|
+
while (current && current !== dirname(current)) {
|
|
235
|
+
const packageJsonPath = join(current, 'package.json')
|
|
236
|
+
if (existsSync(packageJsonPath)) return packageJsonPath
|
|
237
|
+
current = dirname(current)
|
|
238
|
+
}
|
|
239
|
+
return null
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function detectGeminiCliVersion(binaryPath) {
|
|
243
|
+
if (!binaryPath) return null
|
|
244
|
+
try {
|
|
245
|
+
const realPath = realpathSync(binaryPath)
|
|
246
|
+
const versionMatch = realPath.match(/gemini-cli[\\/](\d+\.\d+\.\d+)(?:[\\/]|$)/)
|
|
247
|
+
if (versionMatch?.[1]) return versionMatch[1]
|
|
248
|
+
|
|
249
|
+
const packageJsonPath = findPackageJsonUpwards(realPath)
|
|
250
|
+
if (!packageJsonPath) return null
|
|
251
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
252
|
+
if (typeof pkg?.version === 'string' && pkg.version.length > 0) {
|
|
253
|
+
return pkg.version
|
|
254
|
+
}
|
|
255
|
+
} catch { /* best effort */ }
|
|
256
|
+
return null
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function extractGeminiConfigError(output) {
|
|
260
|
+
const text = String(output || '').trim()
|
|
261
|
+
if (!text.includes('Invalid configuration in ')) return null
|
|
262
|
+
const lines = text.split(/\r?\n/).filter(Boolean)
|
|
263
|
+
return lines.slice(0, 8).join('\n')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function inspectGeminiCliSupport(options = {}) {
|
|
267
|
+
const binaryPath = options.binaryPath || findExecutableOnPath(options.command || 'gemini')
|
|
268
|
+
if (!binaryPath) {
|
|
269
|
+
return {
|
|
270
|
+
installed: false,
|
|
271
|
+
version: null,
|
|
272
|
+
supportsProxyBaseUrl: false,
|
|
273
|
+
configError: null,
|
|
274
|
+
reason: 'Gemini CLI is not installed in PATH.',
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const version = options.version || detectGeminiCliVersion(binaryPath)
|
|
279
|
+
const helpResult = options.helpResult || spawnSync(binaryPath, ['--help'], {
|
|
280
|
+
encoding: 'utf8',
|
|
281
|
+
timeout: 5000,
|
|
282
|
+
env: options.inheritedEnv || process.env,
|
|
283
|
+
})
|
|
284
|
+
const helpOutput = `${helpResult.stdout || ''}\n${helpResult.stderr || ''}`.trim()
|
|
285
|
+
const configError = extractGeminiConfigError(helpOutput)
|
|
286
|
+
const supportsProxyBaseUrl = version ? compareSemver(version, GEMINI_PROXY_MIN_VERSION) >= 0 : false
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
installed: true,
|
|
290
|
+
version,
|
|
291
|
+
supportsProxyBaseUrl,
|
|
292
|
+
configError,
|
|
293
|
+
reason: supportsProxyBaseUrl
|
|
294
|
+
? null
|
|
295
|
+
: `Gemini CLI ${version || '(unknown version)'} does not expose stable custom base URL support for proxy mode yet.`,
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
145
299
|
function spawnCommand(command, args, env) {
|
|
146
300
|
return new Promise((resolve, reject) => {
|
|
147
301
|
const child = spawn(command, args, {
|
|
@@ -347,6 +501,10 @@ function printConfigResult(toolName, result) {
|
|
|
347
501
|
if (result.backupPath) console.log(chalk.dim(` 💾 Backup: ${result.backupPath}`))
|
|
348
502
|
}
|
|
349
503
|
|
|
504
|
+
function printExperimentalProxyNote() {
|
|
505
|
+
console.log(chalk.dim(` ${EXPERIMENTAL_PROXY_TOOLS_NOTE}`))
|
|
506
|
+
}
|
|
507
|
+
|
|
350
508
|
export async function startExternalTool(mode, model, config) {
|
|
351
509
|
const meta = getToolMeta(mode)
|
|
352
510
|
const { env, apiKey, baseUrl } = buildToolEnv(mode, model, config)
|
|
@@ -429,6 +587,7 @@ export async function startExternalTool(mode, model, config) {
|
|
|
429
587
|
console.log()
|
|
430
588
|
console.log(chalk.yellow(' The proxy translates between provider protocols and handles key rotation,'))
|
|
431
589
|
console.log(chalk.yellow(' which is required for this tool to connect.'))
|
|
590
|
+
console.log(chalk.dim(` ${EXPERIMENTAL_PROXY_TOOLS_NOTE}`))
|
|
432
591
|
console.log()
|
|
433
592
|
console.log(chalk.white(' To enable it:'))
|
|
434
593
|
console.log(chalk.dim(' 1. Press ') + chalk.bold.white('J') + chalk.dim(' to open FCM Proxy V2 settings'))
|
|
@@ -441,33 +600,71 @@ export async function startExternalTool(mode, model, config) {
|
|
|
441
600
|
|
|
442
601
|
if (mode === 'claude-code') {
|
|
443
602
|
// 📖 Claude Code needs Anthropic-compatible wire format (POST /v1/messages).
|
|
444
|
-
// 📖
|
|
603
|
+
// 📖 Mirror free-claude-code: one auth env only (`ANTHROPIC_AUTH_TOKEN`) plus base URL.
|
|
445
604
|
const started = await ensureProxyRunning(config)
|
|
605
|
+
const { env: proxyEnv } = buildToolEnv(mode, model, config, {
|
|
606
|
+
sanitize: true,
|
|
607
|
+
includeCompatDefaults: false,
|
|
608
|
+
includeProviderEnv: false,
|
|
609
|
+
})
|
|
446
610
|
const proxyBase = `http://127.0.0.1:${started.port}`
|
|
447
|
-
env.ANTHROPIC_BASE_URL = proxyBase
|
|
448
|
-
env.ANTHROPIC_API_KEY = started.proxyToken
|
|
449
611
|
const launchModelId = resolveLauncherModelId(model, true)
|
|
612
|
+
proxyEnv.ANTHROPIC_BASE_URL = proxyBase
|
|
613
|
+
proxyEnv.ANTHROPIC_AUTH_TOKEN = started.proxyToken
|
|
614
|
+
proxyEnv.ANTHROPIC_MODEL = launchModelId
|
|
450
615
|
console.log(chalk.dim(` 📖 Claude Code routed through FCM proxy on :${started.port} (Anthropic translation enabled)`))
|
|
451
|
-
return spawnCommand('claude', ['--model', launchModelId],
|
|
616
|
+
return spawnCommand('claude', ['--model', launchModelId], proxyEnv)
|
|
452
617
|
}
|
|
453
618
|
|
|
454
619
|
if (mode === 'codex') {
|
|
455
620
|
const started = await ensureProxyRunning(config)
|
|
456
|
-
env
|
|
457
|
-
|
|
621
|
+
const { env: proxyEnv } = buildToolEnv(mode, model, config, {
|
|
622
|
+
sanitize: true,
|
|
623
|
+
includeCompatDefaults: false,
|
|
624
|
+
includeProviderEnv: false,
|
|
625
|
+
})
|
|
458
626
|
const launchModelId = resolveLauncherModelId(model, true)
|
|
627
|
+
const proxyBaseUrl = `http://127.0.0.1:${started.port}/v1`
|
|
628
|
+
proxyEnv.FCM_PROXY_API_KEY = started.proxyToken
|
|
459
629
|
console.log(chalk.dim(` 📖 Codex routed through FCM proxy on :${started.port}`))
|
|
460
|
-
return spawnCommand('codex', ['--model', launchModelId],
|
|
630
|
+
return spawnCommand('codex', [...buildCodexProxyArgs(proxyBaseUrl), '--model', launchModelId], proxyEnv)
|
|
461
631
|
}
|
|
462
632
|
|
|
463
633
|
if (mode === 'gemini') {
|
|
634
|
+
const geminiSupport = inspectGeminiCliSupport()
|
|
635
|
+
if (geminiSupport.configError) {
|
|
636
|
+
console.log()
|
|
637
|
+
console.log(chalk.red(' ✖ Gemini CLI configuration is invalid, so the proxy launch is blocked before auth.'))
|
|
638
|
+
console.log(chalk.dim(` ${geminiSupport.configError.split('\n').join('\n ')}`))
|
|
639
|
+
printExperimentalProxyNote()
|
|
640
|
+
console.log(chalk.dim(' Fix ~/.gemini/settings.json, then try again.'))
|
|
641
|
+
console.log()
|
|
642
|
+
return 1
|
|
643
|
+
}
|
|
644
|
+
if (!geminiSupport.supportsProxyBaseUrl) {
|
|
645
|
+
console.log()
|
|
646
|
+
const versionLabel = geminiSupport.version ? `v${geminiSupport.version}` : 'this installed version'
|
|
647
|
+
console.log(chalk.red(` ✖ Gemini CLI ${versionLabel} is not proxy-compatible in FCM yet.`))
|
|
648
|
+
console.log(chalk.yellow(' This build does not expose the custom base-URL contract we need, so launching it through the proxy would be misleading.'))
|
|
649
|
+
printExperimentalProxyNote()
|
|
650
|
+
console.log(chalk.dim(` Expected: Gemini CLI ${GEMINI_PROXY_MIN_VERSION}+ with stable proxy base URL support.`))
|
|
651
|
+
console.log()
|
|
652
|
+
return 1
|
|
653
|
+
}
|
|
654
|
+
|
|
464
655
|
const started = await ensureProxyRunning(config)
|
|
465
|
-
env.OPENAI_API_KEY = started.proxyToken
|
|
466
|
-
env.OPENAI_BASE_URL = `http://127.0.0.1:${started.port}/v1`
|
|
467
656
|
const launchModelId = resolveLauncherModelId(model, true)
|
|
657
|
+
const { env: proxyEnv } = buildToolEnv(mode, model, config, {
|
|
658
|
+
sanitize: true,
|
|
659
|
+
includeCompatDefaults: false,
|
|
660
|
+
includeProviderEnv: false,
|
|
661
|
+
})
|
|
662
|
+
proxyEnv.GEMINI_API_KEY = started.proxyToken
|
|
663
|
+
proxyEnv.GOOGLE_API_KEY = started.proxyToken
|
|
664
|
+
proxyEnv.GOOGLE_GEMINI_BASE_URL = `http://127.0.0.1:${started.port}/v1`
|
|
468
665
|
printConfigResult(meta.label, writeGeminiConfig({ ...model, modelId: launchModelId }))
|
|
469
666
|
console.log(chalk.dim(` 📖 Gemini routed through FCM proxy on :${started.port}`))
|
|
470
|
-
return spawnCommand('gemini', ['--model', launchModelId],
|
|
667
|
+
return spawnCommand('gemini', ['--model', launchModelId], proxyEnv)
|
|
471
668
|
}
|
|
472
669
|
|
|
473
670
|
if (mode === 'qwen') {
|
package/src/utils.js
CHANGED
|
@@ -386,16 +386,16 @@ export function findBestModel(results) {
|
|
|
386
386
|
// 📖 Slices from index 2 to get user-provided arguments only.
|
|
387
387
|
//
|
|
388
388
|
// 📖 Argument types:
|
|
389
|
-
// - API key: first positional arg that
|
|
389
|
+
// - API key: first positional arg that does not look like a CLI flag (e.g., "nvapi-xxx")
|
|
390
390
|
// - Boolean flags: --best, --fiable, --opencode, --opencode-desktop, --openclaw,
|
|
391
391
|
// --aider, --crush, --goose, --claude-code, --codex, --gemini, --qwen,
|
|
392
|
-
// --openhands, --amp, --pi, --no-telemetry, --json (case-insensitive)
|
|
392
|
+
// --openhands, --amp, --pi, --no-telemetry, --json, --help/-h (case-insensitive)
|
|
393
393
|
// - Value flag: --tier <letter> (the next non-flag arg is the tier value)
|
|
394
394
|
//
|
|
395
395
|
// 📖 Returns:
|
|
396
396
|
// { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openClawMode,
|
|
397
397
|
// aiderMode, crushMode, gooseMode, claudeCodeMode, codexMode, geminiMode,
|
|
398
|
-
// qwenMode, openHandsMode, ampMode, piMode, noTelemetry, jsonMode, tierFilter }
|
|
398
|
+
// qwenMode, openHandsMode, ampMode, piMode, noTelemetry, jsonMode, helpMode, tierFilter }
|
|
399
399
|
//
|
|
400
400
|
// 📖 Note: apiKey may be null here — the main CLI falls back to env vars and saved config.
|
|
401
401
|
export function parseArgs(argv) {
|
|
@@ -420,7 +420,7 @@ export function parseArgs(argv) {
|
|
|
420
420
|
if (profileValueIdx !== -1) skipIndices.add(profileValueIdx)
|
|
421
421
|
|
|
422
422
|
for (const [i, arg] of args.entries()) {
|
|
423
|
-
if (arg.startsWith('--')) {
|
|
423
|
+
if (arg.startsWith('--') || arg === '-h') {
|
|
424
424
|
flags.push(arg.toLowerCase())
|
|
425
425
|
} else if (skipIndices.has(i)) {
|
|
426
426
|
// 📖 Skip — this is a value for --tier or --profile, not an API key
|
|
@@ -447,6 +447,7 @@ export function parseArgs(argv) {
|
|
|
447
447
|
const noTelemetry = flags.includes('--no-telemetry')
|
|
448
448
|
const cleanProxyMode = flags.includes('--clean-proxy') || flags.includes('--proxy-clean')
|
|
449
449
|
const jsonMode = flags.includes('--json')
|
|
450
|
+
const helpMode = flags.includes('--help') || flags.includes('-h')
|
|
450
451
|
|
|
451
452
|
let tierFilter = tierValueIdx !== -1 ? args[tierValueIdx].toUpperCase() : null
|
|
452
453
|
|
|
@@ -475,6 +476,7 @@ export function parseArgs(argv) {
|
|
|
475
476
|
noTelemetry,
|
|
476
477
|
cleanProxyMode,
|
|
477
478
|
jsonMode,
|
|
479
|
+
helpMode,
|
|
478
480
|
tierFilter,
|
|
479
481
|
profileName,
|
|
480
482
|
recommendMode
|
|
@@ -707,17 +709,31 @@ export function getProxyStatusInfo(proxyStartupStatus, isProxyActive, isProxyEna
|
|
|
707
709
|
}
|
|
708
710
|
|
|
709
711
|
/**
|
|
710
|
-
* 📖 getVersionStatusInfo turns
|
|
712
|
+
* 📖 getVersionStatusInfo turns startup + manual update-check state into a compact,
|
|
711
713
|
* 📖 render-friendly footer descriptor for the main table.
|
|
712
714
|
*
|
|
713
|
-
* 📖
|
|
714
|
-
* 📖
|
|
715
|
+
* 📖 Priority:
|
|
716
|
+
* 📖 1. Manual Settings check found an update (`available`)
|
|
717
|
+
* 📖 2. Startup auto-check already found a newer npm version
|
|
718
|
+
* 📖 3. Otherwise stay quiet
|
|
719
|
+
* 📖
|
|
720
|
+
* 📖 `versionAlertsEnabled` lets the CLI suppress npm-specific warnings in dev checkouts,
|
|
721
|
+
* 📖 where telling contributors to run a global npm update would be bogus.
|
|
715
722
|
*
|
|
716
723
|
* @param {'idle'|'checking'|'available'|'up-to-date'|'error'|'installing'} updateState
|
|
717
724
|
* @param {string|null} latestVersion
|
|
725
|
+
* @param {string|null} [startupLatestVersion=null]
|
|
726
|
+
* @param {boolean} [versionAlertsEnabled=true]
|
|
718
727
|
* @returns {{ isOutdated: boolean, latestVersion: string|null }}
|
|
719
728
|
*/
|
|
720
|
-
export function getVersionStatusInfo(updateState, latestVersion) {
|
|
729
|
+
export function getVersionStatusInfo(updateState, latestVersion, startupLatestVersion = null, versionAlertsEnabled = true) {
|
|
730
|
+
if (!versionAlertsEnabled) {
|
|
731
|
+
return {
|
|
732
|
+
isOutdated: false,
|
|
733
|
+
latestVersion: null,
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
721
737
|
if (updateState === 'available' && typeof latestVersion === 'string' && latestVersion.trim()) {
|
|
722
738
|
return {
|
|
723
739
|
isOutdated: true,
|
|
@@ -725,6 +741,13 @@ export function getVersionStatusInfo(updateState, latestVersion) {
|
|
|
725
741
|
}
|
|
726
742
|
}
|
|
727
743
|
|
|
744
|
+
if (typeof startupLatestVersion === 'string' && startupLatestVersion.trim()) {
|
|
745
|
+
return {
|
|
746
|
+
isOutdated: true,
|
|
747
|
+
latestVersion: startupLatestVersion.trim(),
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
728
751
|
return {
|
|
729
752
|
isOutdated: false,
|
|
730
753
|
latestVersion: null,
|