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
package/src/tool-metadata.js
CHANGED
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
*
|
|
22
22
|
* @exports TOOL_METADATA, TOOL_MODE_ORDER, getToolMeta, getToolModeOrder
|
|
23
23
|
*/
|
|
24
|
-
|
|
25
24
|
export const TOOL_METADATA = {
|
|
26
25
|
opencode: { label: 'OpenCode CLI', emoji: '💻', flag: '--opencode' },
|
|
27
26
|
'opencode-desktop': { label: 'OpenCode Desktop', emoji: '🖥', flag: '--opencode-desktop' },
|
|
@@ -30,9 +29,6 @@ export const TOOL_METADATA = {
|
|
|
30
29
|
goose: { label: 'Goose', emoji: '🪿', flag: '--goose' },
|
|
31
30
|
pi: { label: 'Pi', emoji: 'π', flag: '--pi' },
|
|
32
31
|
aider: { label: 'Aider', emoji: '🛠', flag: '--aider' },
|
|
33
|
-
'claude-code': { label: 'Claude Code', emoji: '🧠', flag: '--claude-code' },
|
|
34
|
-
codex: { label: 'Codex CLI', emoji: '⌘', flag: '--codex' },
|
|
35
|
-
gemini: { label: 'Gemini CLI', emoji: '✦', flag: '--gemini' },
|
|
36
32
|
qwen: { label: 'Qwen Code', emoji: '🌊', flag: '--qwen' },
|
|
37
33
|
openhands: { label: 'OpenHands', emoji: '🤲', flag: '--openhands' },
|
|
38
34
|
amp: { label: 'Amp', emoji: '⚡', flag: '--amp' },
|
|
@@ -46,9 +42,6 @@ export const TOOL_MODE_ORDER = [
|
|
|
46
42
|
'goose',
|
|
47
43
|
'pi',
|
|
48
44
|
'aider',
|
|
49
|
-
'claude-code',
|
|
50
|
-
'codex',
|
|
51
|
-
'gemini',
|
|
52
45
|
'qwen',
|
|
53
46
|
'openhands',
|
|
54
47
|
'amp',
|
package/src/utils.js
CHANGED
|
@@ -388,14 +388,14 @@ export function findBestModel(results) {
|
|
|
388
388
|
// 📖 Argument types:
|
|
389
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
|
-
// --aider, --crush, --goose, --
|
|
391
|
+
// --aider, --crush, --goose, --qwen,
|
|
392
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
|
-
// aiderMode, crushMode, gooseMode,
|
|
398
|
-
//
|
|
397
|
+
// aiderMode, crushMode, gooseMode, qwenMode, openHandsMode, ampMode,
|
|
398
|
+
// 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) {
|
|
@@ -450,16 +450,11 @@ export function parseArgs(argv) {
|
|
|
450
450
|
const aiderMode = flags.includes('--aider')
|
|
451
451
|
const crushMode = flags.includes('--crush')
|
|
452
452
|
const gooseMode = flags.includes('--goose')
|
|
453
|
-
const claudeCodeMode = flags.includes('--claude-code')
|
|
454
|
-
const codexMode = flags.includes('--codex')
|
|
455
|
-
const geminiMode = flags.includes('--gemini')
|
|
456
453
|
const qwenMode = flags.includes('--qwen')
|
|
457
454
|
const openHandsMode = flags.includes('--openhands')
|
|
458
455
|
const ampMode = flags.includes('--amp')
|
|
459
456
|
const piMode = flags.includes('--pi')
|
|
460
457
|
const noTelemetry = flags.includes('--no-telemetry')
|
|
461
|
-
const cleanProxyMode = flags.includes('--clean-proxy') || flags.includes('--proxy-clean')
|
|
462
|
-
const proxyForegroundMode = flags.includes('--proxy')
|
|
463
458
|
const jsonMode = flags.includes('--json')
|
|
464
459
|
const helpMode = flags.includes('--help') || flags.includes('-h')
|
|
465
460
|
const premiumMode = flags.includes('--premium')
|
|
@@ -492,15 +487,11 @@ export function parseArgs(argv) {
|
|
|
492
487
|
aiderMode,
|
|
493
488
|
crushMode,
|
|
494
489
|
gooseMode,
|
|
495
|
-
claudeCodeMode,
|
|
496
|
-
codexMode,
|
|
497
|
-
geminiMode,
|
|
498
490
|
qwenMode,
|
|
499
491
|
openHandsMode,
|
|
500
492
|
ampMode,
|
|
501
493
|
piMode,
|
|
502
494
|
noTelemetry,
|
|
503
|
-
cleanProxyMode,
|
|
504
495
|
jsonMode,
|
|
505
496
|
helpMode,
|
|
506
497
|
tierFilter,
|
|
@@ -514,7 +505,6 @@ export function parseArgs(argv) {
|
|
|
514
505
|
premiumMode,
|
|
515
506
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
516
507
|
recommendMode,
|
|
517
|
-
proxyForegroundMode,
|
|
518
508
|
}
|
|
519
509
|
}
|
|
520
510
|
|
|
@@ -688,61 +678,6 @@ export function getTopRecommendations(results, taskType, priority, contextBudget
|
|
|
688
678
|
return scored.slice(0, topN)
|
|
689
679
|
}
|
|
690
680
|
|
|
691
|
-
/**
|
|
692
|
-
* 📖 getProxyStatusInfo: Pure function that maps startup proxy status + active proxy state
|
|
693
|
-
* 📖 to a normalised descriptor object consumed by the TUI footer indicator.
|
|
694
|
-
*
|
|
695
|
-
* 📖 Priority of evaluation:
|
|
696
|
-
* 1. proxyStartupStatus.phase === 'starting' → state:'starting'
|
|
697
|
-
* 2. proxyStartupStatus.phase === 'running' → state:'running' with port/accountCount
|
|
698
|
-
* 3. proxyStartupStatus.phase === 'failed' → state:'failed' with truncated reason
|
|
699
|
-
* 4. isProxyActive (legacy activeProxy flag) → state:'running' (no port detail)
|
|
700
|
-
* 5. isProxyEnabled → state:'configured'
|
|
701
|
-
* 6. otherwise → state:'stopped'
|
|
702
|
-
*
|
|
703
|
-
* 📖 Reason is clamped to 80 characters to keep footer readable (no stack traces).
|
|
704
|
-
*
|
|
705
|
-
* @param {object|null} proxyStartupStatus — state.proxyStartupStatus value
|
|
706
|
-
* @param {boolean} isProxyActive — truthy when the module-level activeProxy is non-null
|
|
707
|
-
* @param {boolean} [isProxyEnabled=false] — truthy when proxy mode is enabled in settings
|
|
708
|
-
* @returns {{ state: string, port?: number, accountCount?: number, reason?: string }}
|
|
709
|
-
*/
|
|
710
|
-
export function getProxyStatusInfo(proxyStartupStatus, isProxyActive, isProxyEnabled = false) {
|
|
711
|
-
const MAX_REASON = 80
|
|
712
|
-
|
|
713
|
-
if (proxyStartupStatus) {
|
|
714
|
-
const { phase } = proxyStartupStatus
|
|
715
|
-
if (phase === 'starting') {
|
|
716
|
-
return { state: 'starting' }
|
|
717
|
-
}
|
|
718
|
-
if (phase === 'running') {
|
|
719
|
-
return {
|
|
720
|
-
state: 'running',
|
|
721
|
-
port: proxyStartupStatus.port,
|
|
722
|
-
accountCount: proxyStartupStatus.accountCount,
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
if (phase === 'failed') {
|
|
726
|
-
const raw = proxyStartupStatus.reason ?? 'unknown error'
|
|
727
|
-
return {
|
|
728
|
-
state: 'failed',
|
|
729
|
-
reason: raw.length > MAX_REASON ? raw.slice(0, MAX_REASON - 1) + '…' : raw,
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// 📖 Legacy fallback: activeProxy set directly (e.g. from manual proxy start without startup status)
|
|
735
|
-
if (isProxyActive) {
|
|
736
|
-
return { state: 'running' }
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
if (isProxyEnabled) {
|
|
740
|
-
return { state: 'configured' }
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
return { state: 'stopped' }
|
|
744
|
-
}
|
|
745
|
-
|
|
746
681
|
/**
|
|
747
682
|
* 📖 getVersionStatusInfo turns startup + manual update-check state into a compact,
|
|
748
683
|
* 📖 render-friendly footer descriptor for the main table.
|
package/bin/fcm-proxy-daemon.js
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file bin/fcm-proxy-daemon.js
|
|
5
|
-
* @description Standalone headless FCM proxy daemon — runs independently of the TUI.
|
|
6
|
-
*
|
|
7
|
-
* 📖 This is the always-on background proxy server. It reads the user's config
|
|
8
|
-
* (~/.free-coding-models.json), builds the proxy topology (merged models × API keys),
|
|
9
|
-
* and starts a ProxyServer on a stable port with a stable token.
|
|
10
|
-
*
|
|
11
|
-
* 📖 When installed as a launchd LaunchAgent (macOS) or systemd user service (Linux),
|
|
12
|
-
* this daemon starts at login and persists across reboots, allowing Claude Code,
|
|
13
|
-
* Gemini CLI, OpenCode, and all other tools to access free models 24/7.
|
|
14
|
-
*
|
|
15
|
-
* 📖 Status file: ~/.free-coding-models/daemon.json
|
|
16
|
-
* Contains PID, port, token, version, model/account counts. The TUI reads this
|
|
17
|
-
* to detect a running daemon and delegate instead of starting an in-process proxy.
|
|
18
|
-
*
|
|
19
|
-
* 📖 Hot-reload: Watches ~/.free-coding-models.json for changes and reloads the
|
|
20
|
-
* proxy topology (accounts, models) without restarting the process.
|
|
21
|
-
*
|
|
22
|
-
* @see src/proxy-topology.js — shared topology builder
|
|
23
|
-
* @see src/proxy-server.js — ProxyServer implementation
|
|
24
|
-
* @see src/daemon-manager.js — install/uninstall/status management
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, watch } from 'node:fs'
|
|
28
|
-
import { join } from 'node:path'
|
|
29
|
-
import { homedir } from 'node:os'
|
|
30
|
-
import { createRequire } from 'node:module'
|
|
31
|
-
import { fileURLToPath } from 'node:url'
|
|
32
|
-
|
|
33
|
-
// 📖 Resolve package.json for version info
|
|
34
|
-
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
35
|
-
let PKG_VERSION = 'unknown'
|
|
36
|
-
try {
|
|
37
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'))
|
|
38
|
-
PKG_VERSION = pkg.version || 'unknown'
|
|
39
|
-
} catch { /* ignore */ }
|
|
40
|
-
|
|
41
|
-
// 📖 Config + data paths
|
|
42
|
-
const CONFIG_PATH = join(homedir(), '.free-coding-models.json')
|
|
43
|
-
const DATA_DIR = join(homedir(), '.free-coding-models')
|
|
44
|
-
const DAEMON_STATUS_FILE = join(DATA_DIR, 'daemon.json')
|
|
45
|
-
const LOG_PREFIX = '[fcm-daemon]'
|
|
46
|
-
|
|
47
|
-
// 📖 Default daemon port — high port unlikely to conflict
|
|
48
|
-
const DEFAULT_DAEMON_PORT = 18045
|
|
49
|
-
|
|
50
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
function log(msg) {
|
|
53
|
-
console.log(`${LOG_PREFIX} ${new Date().toISOString()} ${msg}`)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function logError(msg) {
|
|
57
|
-
console.error(`${LOG_PREFIX} ${new Date().toISOString()} ERROR: ${msg}`)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 📖 Write daemon status file so TUI and tools can discover the running daemon.
|
|
62
|
-
*/
|
|
63
|
-
function writeDaemonStatus(info) {
|
|
64
|
-
if (!existsSync(DATA_DIR)) {
|
|
65
|
-
mkdirSync(DATA_DIR, { mode: 0o700, recursive: true })
|
|
66
|
-
}
|
|
67
|
-
writeFileSync(DAEMON_STATUS_FILE, JSON.stringify(info, null, 2), { mode: 0o600 })
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 📖 Remove daemon status file on shutdown.
|
|
72
|
-
*/
|
|
73
|
-
function removeDaemonStatus() {
|
|
74
|
-
try {
|
|
75
|
-
if (existsSync(DAEMON_STATUS_FILE)) unlinkSync(DAEMON_STATUS_FILE)
|
|
76
|
-
} catch { /* best-effort */ }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
async function main() {
|
|
82
|
-
log(`Starting FCM Proxy V2 v${PKG_VERSION} (PID: ${process.pid})`)
|
|
83
|
-
|
|
84
|
-
// 📖 Dynamic imports — keep startup fast, avoid loading TUI-specific modules
|
|
85
|
-
const { loadConfig, getProxySettings } = await import('../src/config.js')
|
|
86
|
-
const { ProxyServer } = await import('../src/proxy-server.js')
|
|
87
|
-
const { buildProxyTopologyFromConfig, buildMergedModelsForDaemon } = await import('../src/proxy-topology.js')
|
|
88
|
-
const { sources } = await import('../sources.js')
|
|
89
|
-
|
|
90
|
-
// 📖 Load config and build initial topology — wrapped in try/catch to provide clear error on startup failures
|
|
91
|
-
let fcmConfig, proxySettings, mergedModels, accounts, proxyModels, anthropicRouting
|
|
92
|
-
try {
|
|
93
|
-
fcmConfig = loadConfig()
|
|
94
|
-
proxySettings = getProxySettings(fcmConfig)
|
|
95
|
-
} catch (err) {
|
|
96
|
-
logError(`Fatal: Failed to load config: ${err.message}`)
|
|
97
|
-
process.exit(1)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (!proxySettings.stableToken) {
|
|
101
|
-
logError('No stableToken in proxy settings — run the TUI first to initialize config.')
|
|
102
|
-
process.exit(1)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const port = proxySettings.preferredPort || DEFAULT_DAEMON_PORT
|
|
106
|
-
const token = proxySettings.stableToken
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
log(`Building merged model catalog...`)
|
|
110
|
-
mergedModels = await buildMergedModelsForDaemon()
|
|
111
|
-
log(`Merged ${mergedModels.length} model groups`)
|
|
112
|
-
|
|
113
|
-
const topology = buildProxyTopologyFromConfig(fcmConfig, mergedModels, sources)
|
|
114
|
-
accounts = topology.accounts
|
|
115
|
-
proxyModels = topology.proxyModels
|
|
116
|
-
anthropicRouting = topology.anthropicRouting
|
|
117
|
-
} catch (err) {
|
|
118
|
-
logError(`Fatal: Failed to build initial topology: ${err.message}`)
|
|
119
|
-
process.exit(1)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (accounts.length === 0) {
|
|
123
|
-
logError('No API keys configured — FCM Proxy V2 has no accounts to serve. Add keys via the TUI.')
|
|
124
|
-
process.exit(1)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
log(`Built proxy topology: ${accounts.length} accounts across ${Object.keys(proxyModels).length} models`)
|
|
128
|
-
|
|
129
|
-
// 📖 Start the proxy server
|
|
130
|
-
const proxy = new ProxyServer({
|
|
131
|
-
port,
|
|
132
|
-
accounts,
|
|
133
|
-
proxyApiKey: token,
|
|
134
|
-
anthropicRouting,
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
const { port: listeningPort } = await proxy.start()
|
|
139
|
-
log(`Proxy listening on 127.0.0.1:${listeningPort}`)
|
|
140
|
-
|
|
141
|
-
// 📖 Write status file for TUI discovery
|
|
142
|
-
const statusInfo = {
|
|
143
|
-
pid: process.pid,
|
|
144
|
-
port: listeningPort,
|
|
145
|
-
token,
|
|
146
|
-
startedAt: new Date().toISOString(),
|
|
147
|
-
version: PKG_VERSION,
|
|
148
|
-
modelCount: Object.keys(proxyModels).length,
|
|
149
|
-
accountCount: accounts.length,
|
|
150
|
-
}
|
|
151
|
-
writeDaemonStatus(statusInfo)
|
|
152
|
-
log(`Status file written to ${DAEMON_STATUS_FILE}`)
|
|
153
|
-
|
|
154
|
-
// 📖 Set up config file watcher for hot-reload
|
|
155
|
-
let reloadTimeout = null
|
|
156
|
-
// 📖 Prevents concurrent reloads — if a reload is in progress, the next
|
|
157
|
-
// watcher event will be queued (one pending max) instead of stacking
|
|
158
|
-
let reloadInProgress = false
|
|
159
|
-
let reloadQueued = false
|
|
160
|
-
const configWatcher = watch(CONFIG_PATH, () => {
|
|
161
|
-
// 📖 Debounce 1s — config writes can trigger multiple fs events
|
|
162
|
-
if (reloadTimeout) clearTimeout(reloadTimeout)
|
|
163
|
-
reloadTimeout = setTimeout(async () => {
|
|
164
|
-
if (reloadInProgress) {
|
|
165
|
-
reloadQueued = true
|
|
166
|
-
return
|
|
167
|
-
}
|
|
168
|
-
reloadInProgress = true
|
|
169
|
-
try {
|
|
170
|
-
log('Config file changed — reloading topology...')
|
|
171
|
-
fcmConfig = loadConfig()
|
|
172
|
-
mergedModels = await buildMergedModelsForDaemon()
|
|
173
|
-
const newTopology = buildProxyTopologyFromConfig(fcmConfig, mergedModels, sources)
|
|
174
|
-
|
|
175
|
-
if (newTopology.accounts.length === 0) {
|
|
176
|
-
log('Warning: new topology has 0 accounts — keeping current topology')
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
proxy.updateAccounts(newTopology.accounts, newTopology.anthropicRouting)
|
|
181
|
-
accounts = newTopology.accounts
|
|
182
|
-
proxyModels = newTopology.proxyModels
|
|
183
|
-
anthropicRouting = newTopology.anthropicRouting
|
|
184
|
-
|
|
185
|
-
// 📖 Update status file
|
|
186
|
-
writeDaemonStatus({
|
|
187
|
-
...statusInfo,
|
|
188
|
-
modelCount: Object.keys(proxyModels).length,
|
|
189
|
-
accountCount: accounts.length,
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
log(`Topology reloaded: ${accounts.length} accounts, ${Object.keys(proxyModels).length} models`)
|
|
193
|
-
} catch (err) {
|
|
194
|
-
logError(`Hot-reload failed: ${err.message}`)
|
|
195
|
-
} finally {
|
|
196
|
-
reloadInProgress = false
|
|
197
|
-
// 📖 If another reload was queued during this one, trigger it now
|
|
198
|
-
if (reloadQueued) {
|
|
199
|
-
reloadQueued = false
|
|
200
|
-
configWatcher.emit('change')
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}, 1000)
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
// 📖 Graceful shutdown
|
|
207
|
-
const shutdown = async (signal) => {
|
|
208
|
-
log(`Received ${signal} — shutting down...`)
|
|
209
|
-
if (reloadTimeout) clearTimeout(reloadTimeout)
|
|
210
|
-
configWatcher.close()
|
|
211
|
-
try {
|
|
212
|
-
await proxy.stop()
|
|
213
|
-
} catch { /* best-effort */ }
|
|
214
|
-
removeDaemonStatus()
|
|
215
|
-
log('FCM Proxy V2 stopped cleanly.')
|
|
216
|
-
process.exit(0)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
process.on('SIGINT', () => shutdown('SIGINT'))
|
|
220
|
-
process.on('SIGTERM', () => shutdown('SIGTERM'))
|
|
221
|
-
process.on('exit', () => removeDaemonStatus())
|
|
222
|
-
|
|
223
|
-
// 📖 Keep the process alive
|
|
224
|
-
log('FCM Proxy V2 ready. Waiting for requests...')
|
|
225
|
-
|
|
226
|
-
} catch (err) {
|
|
227
|
-
if (err.code === 'EADDRINUSE') {
|
|
228
|
-
logError(`Port ${port} is already in use. Another FCM Proxy V2 instance may be running, or another process occupies this port.`)
|
|
229
|
-
logError(`Change proxy.preferredPort in ~/.free-coding-models.json or stop the conflicting process.`)
|
|
230
|
-
process.exit(2)
|
|
231
|
-
}
|
|
232
|
-
logError(`Failed to start proxy: ${err.message}`)
|
|
233
|
-
removeDaemonStatus()
|
|
234
|
-
process.exit(1)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
main().catch(err => {
|
|
239
|
-
logError(`Fatal: ${err.message}`)
|
|
240
|
-
removeDaemonStatus()
|
|
241
|
-
process.exit(1)
|
|
242
|
-
})
|