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.
@@ -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, --claude-code, --codex, --gemini, --qwen,
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, claudeCodeMode, codexMode, geminiMode,
398
- // qwenMode, openHandsMode, ampMode, piMode, noTelemetry, jsonMode, helpMode, tierFilter }
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.
@@ -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
- })