free-coding-models 0.3.9 → 0.3.12

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) {
@@ -403,17 +403,12 @@ export function parseArgs(argv) {
403
403
  let apiKey = null
404
404
  const flags = []
405
405
 
406
- // 📖 Determine which arg indices are consumed by --tier and --profile so we skip them
406
+ // 📖 Determine which arg indices are consumed by --tier so we skip them
407
407
  const tierIdx = args.findIndex(a => a.toLowerCase() === '--tier')
408
408
  const tierValueIdx = (tierIdx !== -1 && args[tierIdx + 1] && !args[tierIdx + 1].startsWith('--'))
409
409
  ? tierIdx + 1
410
410
  : -1
411
411
 
412
- const profileIdx = args.findIndex(a => a.toLowerCase() === '--profile')
413
- const profileValueIdx = (profileIdx !== -1 && args[profileIdx + 1] && !args[profileIdx + 1].startsWith('--'))
414
- ? profileIdx + 1
415
- : -1
416
-
417
412
  // New value flags
418
413
  const sortIdx = args.findIndex(a => a.toLowerCase() === '--sort')
419
414
  const sortValueIdx = (sortIdx !== -1 && args[sortIdx + 1] && !args[sortIdx + 1].startsWith('--'))
@@ -433,7 +428,6 @@ export function parseArgs(argv) {
433
428
  // 📖 Set of arg indices that are values for flags (not API keys)
434
429
  const skipIndices = new Set()
435
430
  if (tierValueIdx !== -1) skipIndices.add(tierValueIdx)
436
- if (profileValueIdx !== -1) skipIndices.add(profileValueIdx)
437
431
  if (sortValueIdx !== -1) skipIndices.add(sortValueIdx)
438
432
  if (originValueIdx !== -1) skipIndices.add(originValueIdx)
439
433
  if (pingIntervalValueIdx !== -1) skipIndices.add(pingIntervalValueIdx)
@@ -442,7 +436,7 @@ export function parseArgs(argv) {
442
436
  if (arg.startsWith('--') || arg === '-h') {
443
437
  flags.push(arg.toLowerCase())
444
438
  } else if (skipIndices.has(i)) {
445
- // 📖 Skip — this is a value for --tier or --profile, not an API key
439
+ // 📖 Skip — this is a value for --tier, not an API key
446
440
  } else if (!apiKey) {
447
441
  apiKey = arg
448
442
  }
@@ -456,15 +450,11 @@ export function parseArgs(argv) {
456
450
  const aiderMode = flags.includes('--aider')
457
451
  const crushMode = flags.includes('--crush')
458
452
  const gooseMode = flags.includes('--goose')
459
- const claudeCodeMode = flags.includes('--claude-code')
460
- const codexMode = flags.includes('--codex')
461
- const geminiMode = flags.includes('--gemini')
462
453
  const qwenMode = flags.includes('--qwen')
463
454
  const openHandsMode = flags.includes('--openhands')
464
455
  const ampMode = flags.includes('--amp')
465
456
  const piMode = flags.includes('--pi')
466
457
  const noTelemetry = flags.includes('--no-telemetry')
467
- const cleanProxyMode = flags.includes('--clean-proxy') || flags.includes('--proxy-clean')
468
458
  const jsonMode = flags.includes('--json')
469
459
  const helpMode = flags.includes('--help') || flags.includes('-h')
470
460
  const premiumMode = flags.includes('--premium')
@@ -482,7 +472,7 @@ export function parseArgs(argv) {
482
472
  let pingInterval = pingIntervalValueIdx !== -1 ? parseInt(args[pingIntervalValueIdx], 10) : null
483
473
  let sortDirection = sortDesc ? 'desc' : (sortAscFlag ? 'asc' : null)
484
474
 
485
- const profileName = profileValueIdx !== -1 ? args[profileValueIdx] : null
475
+ // 📖 Profile system removed - API keys now persist permanently across all sessions
486
476
 
487
477
  // 📖 --recommend — launch directly into Smart Recommend mode (Q key equivalent)
488
478
  const recommendMode = flags.includes('--recommend')
@@ -497,15 +487,11 @@ export function parseArgs(argv) {
497
487
  aiderMode,
498
488
  crushMode,
499
489
  gooseMode,
500
- claudeCodeMode,
501
- codexMode,
502
- geminiMode,
503
490
  qwenMode,
504
491
  openHandsMode,
505
492
  ampMode,
506
493
  piMode,
507
494
  noTelemetry,
508
- cleanProxyMode,
509
495
  jsonMode,
510
496
  helpMode,
511
497
  tierFilter,
@@ -517,8 +503,8 @@ export function parseArgs(argv) {
517
503
  showUnconfigured,
518
504
  disableWidthsWarning,
519
505
  premiumMode,
520
- profileName,
521
- recommendMode
506
+ // 📖 Profile system removed - API keys now persist permanently across all sessions
507
+ recommendMode,
522
508
  }
523
509
  }
524
510
 
@@ -692,61 +678,6 @@ export function getTopRecommendations(results, taskType, priority, contextBudget
692
678
  return scored.slice(0, topN)
693
679
  }
694
680
 
695
- /**
696
- * 📖 getProxyStatusInfo: Pure function that maps startup proxy status + active proxy state
697
- * 📖 to a normalised descriptor object consumed by the TUI footer indicator.
698
- *
699
- * 📖 Priority of evaluation:
700
- * 1. proxyStartupStatus.phase === 'starting' → state:'starting'
701
- * 2. proxyStartupStatus.phase === 'running' → state:'running' with port/accountCount
702
- * 3. proxyStartupStatus.phase === 'failed' → state:'failed' with truncated reason
703
- * 4. isProxyActive (legacy activeProxy flag) → state:'running' (no port detail)
704
- * 5. isProxyEnabled → state:'configured'
705
- * 6. otherwise → state:'stopped'
706
- *
707
- * 📖 Reason is clamped to 80 characters to keep footer readable (no stack traces).
708
- *
709
- * @param {object|null} proxyStartupStatus — state.proxyStartupStatus value
710
- * @param {boolean} isProxyActive — truthy when the module-level activeProxy is non-null
711
- * @param {boolean} [isProxyEnabled=false] — truthy when proxy mode is enabled in settings
712
- * @returns {{ state: string, port?: number, accountCount?: number, reason?: string }}
713
- */
714
- export function getProxyStatusInfo(proxyStartupStatus, isProxyActive, isProxyEnabled = false) {
715
- const MAX_REASON = 80
716
-
717
- if (proxyStartupStatus) {
718
- const { phase } = proxyStartupStatus
719
- if (phase === 'starting') {
720
- return { state: 'starting' }
721
- }
722
- if (phase === 'running') {
723
- return {
724
- state: 'running',
725
- port: proxyStartupStatus.port,
726
- accountCount: proxyStartupStatus.accountCount,
727
- }
728
- }
729
- if (phase === 'failed') {
730
- const raw = proxyStartupStatus.reason ?? 'unknown error'
731
- return {
732
- state: 'failed',
733
- reason: raw.length > MAX_REASON ? raw.slice(0, MAX_REASON - 1) + '…' : raw,
734
- }
735
- }
736
- }
737
-
738
- // 📖 Legacy fallback: activeProxy set directly (e.g. from manual proxy start without startup status)
739
- if (isProxyActive) {
740
- return { state: 'running' }
741
- }
742
-
743
- if (isProxyEnabled) {
744
- return { state: 'configured' }
745
- }
746
-
747
- return { state: 'stopped' }
748
- }
749
-
750
681
  /**
751
682
  * 📖 getVersionStatusInfo turns startup + manual update-check state into a compact,
752
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
- })