free-coding-models 0.3.55 → 0.3.57

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +55 -56
  2. package/README.md +214 -160
  3. package/bin/free-coding-models.js +46 -0
  4. package/package.json +2 -2
  5. package/sources.js +134 -310
  6. package/src/analysis.js +23 -10
  7. package/src/app.js +66 -27
  8. package/src/cache.js +1 -1
  9. package/src/cli-help.js +9 -0
  10. package/src/command-palette.js +15 -13
  11. package/src/config.js +201 -35
  12. package/src/constants.js +4 -4
  13. package/src/endpoint-installer.js +45 -1
  14. package/src/favorites.js +22 -0
  15. package/src/graphify-out/cache/089db1c1def873cf6d112f1590da4490e61e691aff0db41e006aa2fb15ba0656.json +1 -0
  16. package/src/graphify-out/cache/0b510b53cf1a1393fb52b1fc3bbbf88b63938e961ec5b82119a2e9715fee8bd7.json +1 -0
  17. package/src/graphify-out/cache/0ec9a95a326bde58e0316889018b278062d06d494d0f31ba177c9de71e5fed2d.json +1 -0
  18. package/src/graphify-out/cache/1548663a24a68dce740ebab1bd1d3091048c9604e9d067a1650a42a6d82541d4.json +1 -0
  19. package/src/graphify-out/cache/1783af63cb6d0dfb4d469009f71ac83a74ba0b33d48186ff2c6e63f9429e900a.json +1 -0
  20. package/src/graphify-out/cache/1e109f5eb5dc4fd285871c3613e32b6b14a8c225f4080ee34b51c7e1a1764571.json +1 -0
  21. package/src/graphify-out/cache/1eb24dbeb69b46c8bc1caf925df2f2a964af0f33aea143adf8ddf88e017db6ca.json +1 -0
  22. package/src/graphify-out/cache/21e1bcfed11685e8347243f9d8516072dda183266a4bfe22c52fb31753a446c8.json +1 -0
  23. package/src/graphify-out/cache/2327473478b9c4b1940bf7ef66c9ee960b3cba8d5302e56b625df8274246e0b4.json +1 -0
  24. package/src/graphify-out/cache/25955b81fd25454c8fa90fb71a47db8d1215cf621beb8ff3cbd580aaf011b4f3.json +1 -0
  25. package/src/graphify-out/cache/2739677f19c702f88f3de0a0bac475066adbda98709907ad3de967aef689f86d.json +1 -0
  26. package/src/graphify-out/cache/2bba03422f6b3ee7f5b5d29cc90314a064d259e5822a176657bda3e04505cf00.json +1 -0
  27. package/src/graphify-out/cache/2ddf1d2c6d10147b0402446bc71a7988187b79b6210dd7e7250be8c555b9ff35.json +1 -0
  28. package/src/graphify-out/cache/2ee07457a5767c95a57f8e9eb95b28f800044f35666e0715e9d88ad1103a092e.json +1 -0
  29. package/src/graphify-out/cache/2fe9f75dc2951c417f2c8dd22749092cf550dc67599f1c8d1866900dc6e9154e.json +1 -0
  30. package/src/graphify-out/cache/41c4b7c27e7fc3e2948d3a4bf95a72de2ed9a6f0463994babdce8ed2cc84598c.json +1 -0
  31. package/src/graphify-out/cache/5028defd54b7fbd3c7e444973e493de036e097e9b1d2a7cae7f19b88d68aacde.json +1 -0
  32. package/src/graphify-out/cache/5b133aba3fb16410c5b1fdbd1730039fc7fa1ac93abd99d7be08f60da70fc8d4.json +1 -0
  33. package/src/graphify-out/cache/74252e5b0978d85ab3421a3de1a9384aa282ffd2be2cfe7db2530139089f4275.json +1 -0
  34. package/src/graphify-out/cache/7695ebeea056095edd14332963cc43354ef3a097caf46f1e28d0f01369642901.json +1 -0
  35. package/src/graphify-out/cache/777aa7085c395a935c6556bbde182cd871edb61f3a685ed8068ec0c8f6fb0075.json +1 -0
  36. package/src/graphify-out/cache/82a723881980e82273c113def8315533d7da28827e300413d9ad30f27b7407df.json +1 -0
  37. package/src/graphify-out/cache/86b87c9603e6cd188f42c7eed3b86c291d48a781c223a707e74f3e7ed0c02a21.json +1 -0
  38. package/src/graphify-out/cache/890fead9a78cadaed560a2d2453916121fa605c3e43a334910ac4bc951a9ef6d.json +1 -0
  39. package/src/graphify-out/cache/89d3ea66f52783caa775ef9a30923d7d6225e1d8ae9e962f4741b8c7785dab1e.json +1 -0
  40. package/src/graphify-out/cache/8cc82cd9edce41f0e1c092f14a94fd52bf847addf3237b616dc5a9e505bd05bd.json +1 -0
  41. package/src/graphify-out/cache/93ba2e25e3ff7ad525f397902345fbd375df7315de7b402e20cc803c14eccde8.json +1 -0
  42. package/src/graphify-out/cache/99beed29580b9c7bfecfee794cb3d8e535fcf0eb3b92113108f88bdd0a8e79b3.json +1 -0
  43. package/src/graphify-out/cache/aeeb931fa477c65ce2e51d8149957350fa54225c613222bbbe8448998d1afd3d.json +1 -0
  44. package/src/graphify-out/cache/baf91bef5b5ecb2a476433b6cc0c48c563c54ee2d07fc3c192e543685e3e7222.json +1 -0
  45. package/src/graphify-out/cache/bd98b94ac4e9b92b6336d47b26e0366b51a4eaf0711d722f05f98dfae23ab42b.json +1 -0
  46. package/src/graphify-out/cache/bfcb51e9328e9cbfbee4f6fee0f56635d7b03488addc9f6c4e4b190b70a73362.json +1 -0
  47. package/src/graphify-out/cache/c0d3dabeb093aa758c49eadf41b87ecc96a16c1449c2670aaf48cbfc891d8da6.json +1 -0
  48. package/src/graphify-out/cache/c20d6630236f473c1406068c3ae205853e649b216495c93dfec055dd222c55cf.json +1 -0
  49. package/src/graphify-out/cache/c22b9122816bebce0a2f79af41a986559d01e00163dbcd579c5755621b4cb483.json +1 -0
  50. package/src/graphify-out/cache/ca556ec14453ddb8f9e0c5a832dac90d77111b9bad5f8c2d80d272e2e7a06371.json +1 -0
  51. package/src/graphify-out/cache/d6dbc9135dfa35a756b3b09b06700e4bc229fdccba11bb963f2ba44028e0bbae.json +1 -0
  52. package/src/graphify-out/cache/e1cf71276f1779d0fa075f79bd7c8a9fd0b8eef6932ac043137451b7c7fa7cbe.json +1 -0
  53. package/src/graphify-out/cache/e4b3be14494467df2d2ed389bc4f18f099021cb5bc355b901fa88387b2d8b8a2.json +1 -0
  54. package/src/graphify-out/cache/eaea0dded097f6f9553b654220046c6ec0c9be592a5973d906564ee60af34e0d.json +1 -0
  55. package/src/graphify-out/cache/ef07d0cd2675d1f79d2a2fdbf3bc3319687638751e9ce89b0d0d97ed1cd9f7e1.json +1 -0
  56. package/src/graphify-out/cache/f81272d6eb8aaff9e96d5a1d9f06777db70ac3652a646b951ded51f79871d733.json +1 -0
  57. package/src/graphify-out/cache/f9619dd92186f75a6dbda937e0c606647153918524cdb5763f956e6ec2a9e386.json +1 -0
  58. package/src/graphify-out/cache/fd88b1b2ff4bfcae08559d9c2aaeeb9a3f1e2f5cd8928762c311196956c170a5.json +1 -0
  59. package/src/key-handler.js +322 -114
  60. package/src/kilo.js +20 -1
  61. package/src/opencode.js +23 -2
  62. package/src/overlays.js +199 -98
  63. package/src/provider-metadata.js +26 -17
  64. package/src/quota-capabilities.js +6 -10
  65. package/src/render-helpers.js +38 -8
  66. package/src/render-table.js +119 -248
  67. package/src/router-daemon.js +1986 -0
  68. package/src/router-dashboard.js +902 -0
  69. package/src/sync-set.js +479 -0
  70. package/src/theme.js +4 -0
  71. package/src/tool-launchers.js +1 -0
  72. package/src/tool-metadata.js +6 -2
  73. package/src/utils.js +30 -6
  74. package/web/dist/assets/{index-C03JjCgA.js → index-DKHCzbK1.js} +2 -2
  75. package/web/dist/index.html +1 -1
package/src/analysis.js CHANGED
@@ -138,32 +138,45 @@ export function filterByTierOrExit(results, tierLetter) {
138
138
  const OPENROUTER_TIER_MAP = {
139
139
  'qwen/qwen3-coder': ['S+', '70.6%'],
140
140
  'mistralai/devstral-2': ['S+', '72.2%'],
141
- 'stepfun/step-3.5-flash': ['S+', '74.4%'],
142
- 'deepseek/deepseek-r1-0528': ['S', '61.0%'],
141
+ 'minimax/minimax-m2.5': ['S+', '74.0%'],
142
+ 'z-ai/glm-4.5-air': ['S+', '72.0%'],
143
+ 'tencent/hy3-preview': ['S+', '70.0%'],
144
+ 'poolside/laguna-m.1': ['S+', '70.0%'],
145
+ 'poolside/laguna-xs.2': ['S+', '70.0%'],
143
146
  'qwen/qwen3-next-80b-a3b-instruct': ['S', '65.0%'],
144
147
  'openai/gpt-oss-120b': ['S', '60.0%'],
148
+ 'inclusionai/ling-2.6-1t': ['S', '60.0%'],
149
+ 'nvidia/nemotron-3-super-120b-a12b': ['A+', '56.0%'],
150
+ 'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning': ['A+', '52.0%'],
145
151
  'openai/gpt-oss-20b': ['A', '42.0%'],
146
152
  'nvidia/nemotron-3-nano-30b-a3b': ['A', '43.0%'],
147
153
  'meta-llama/llama-3.3-70b-instruct': ['A-', '39.5%'],
148
- 'mimo-v2-flash': ['A', '45.0%'],
154
+ 'google/gemma-4-31b-it': ['A', '45.0%'],
155
+ 'google/gemma-4-26b-a4b-it': ['A-', '38.0%'],
149
156
  'google/gemma-3-27b-it': ['A-', '36.0%'],
150
157
  'google/gemma-3-12b-it': ['B+', '30.0%'],
151
158
  'google/gemma-3-4b-it': ['B', '22.0%'],
152
159
  'google/gemma-3n-e4b-it': ['B', '22.0%'],
153
160
  'google/gemma-3n-e2b-it': ['B', '18.0%'],
154
161
  'meta-llama/llama-3.2-3b-instruct': ['B', '20.0%'],
155
- 'mistralai/mistral-small-3.1-24b-instruct': ['A-', '35.0%'],
156
- 'qwen/qwen3-4b': ['B', '22.0%'],
157
162
  'nousresearch/hermes-3-llama-3.1-405b': ['A', '40.0%'],
158
163
  'nvidia/nemotron-nano-9b-v2': ['B+', '28.0%'],
159
164
  'nvidia/nemotron-nano-12b-v2-vl': ['B+', '30.0%'],
160
- 'z-ai/glm-4.5-air': ['A-', '38.0%'],
161
- 'arcee-ai/trinity-large-preview': ['A', '40.0%'],
162
- 'arcee-ai/trinity-mini': ['B+', '28.0%'],
163
- 'upstage/solar-pro-3': ['A-', '35.0%'],
164
165
  'cognitivecomputations/dolphin-mistral-24b-venice-edition': ['B+', '28.0%'],
165
166
  'liquid/lfm-2.5-1.2b-thinking': ['B', '18.0%'],
166
167
  'liquid/lfm-2.5-1.2b-instruct': ['B', '18.0%'],
168
+ 'openrouter/free': ['B', '25.0%'],
169
+ 'openrouter/owl-alpha': ['A+', '50.0%'],
170
+ }
171
+
172
+ function isOpenRouterFreeModel(model) {
173
+ if (!model?.id) return false
174
+ if (model.id.endsWith(':free')) return true
175
+ const promptPrice = Number(model.pricing?.prompt)
176
+ const completionPrice = Number(model.pricing?.completion)
177
+ return Number.isFinite(promptPrice) && Number.isFinite(completionPrice)
178
+ && promptPrice === 0
179
+ && completionPrice === 0
167
180
  }
168
181
 
169
182
  // 📖 fetchOpenRouterFreeModels: Fetch live free models from OpenRouter API.
@@ -186,7 +199,7 @@ export async function fetchOpenRouterFreeModels() {
186
199
  const json = await res.json()
187
200
  if (!json.data || !Array.isArray(json.data)) return null
188
201
 
189
- const freeModels = json.data.filter(m => m.id && m.id.endsWith(':free'))
202
+ const freeModels = json.data.filter(isOpenRouterFreeModel)
190
203
 
191
204
  return freeModels.map(m => {
192
205
  const baseId = m.id.replace(/:free$/, '')
package/src/app.js CHANGED
@@ -19,7 +19,7 @@
19
19
  * - Direct mode flags plus an in-app Z-cycle for the public launcher set
20
20
  * - Automatic config detection and model setup for both tools
21
21
  * - JSON config stored in ~/.free-coding-models.json (auto-migrates from old plain-text)
22
- * - Multi-provider support via sources.js (NIM/Groq/Cerebras/OpenRouter/Hugging Face/Replicate/DeepInfra/... — extensible)
22
+ * - Multi-provider support via sources.js (NIM/Groq/Cerebras/GitHub Models/Mistral/OpenRouter/... — extensible)
23
23
  * - Settings screen (P key) to manage API keys, provider toggles, manual updates, and provider-key diagnostics
24
24
  * - Install Endpoints flow (Settings / Command Palette) to push provider catalogs into OpenCode, OpenClaw, Crush, and Goose
25
25
  * - Favorites system: toggle with F, switch pinning mode with Y, persist between sessions
@@ -59,7 +59,7 @@
59
59
  * ⚙️ Configuration:
60
60
  * - API keys stored per-provider in ~/.free-coding-models.json (0600 perms)
61
61
  * - Old ~/.free-coding-models plain-text auto-migrated as nvidia key on first run
62
- * - Env vars override config: NVIDIA_API_KEY, GROQ_API_KEY, CEREBRAS_API_KEY, OPENROUTER_API_KEY, HUGGINGFACE_API_KEY/HF_TOKEN, REPLICATE_API_TOKEN, DEEPINFRA_API_KEY/DEEPINFRA_TOKEN, FIREWORKS_API_KEY, SILICONFLOW_API_KEY, TOGETHER_API_KEY, PERPLEXITY_API_KEY, ZAI_API_KEY, etc.
62
+ * - Env vars override config: NVIDIA_API_KEY, GROQ_API_KEY, CEREBRAS_API_KEY, OPENROUTER_API_KEY, GITHUB_TOKEN, MISTRAL_API_KEY, SCALEWAY_API_KEY, GOOGLE_API_KEY, CLOUDFLARE_API_TOKEN, DASHSCOPE_API_KEY, ZAI_API_KEY, etc.
63
63
  * - ZAI (z.ai) uses a non-standard base path; cloudflare needs CLOUDFLARE_ACCOUNT_ID in env.
64
64
  * - Cloudflare Workers AI requires both CLOUDFLARE_API_TOKEN (or CLOUDFLARE_API_KEY) and CLOUDFLARE_ACCOUNT_ID
65
65
  * - Models loaded from sources.js — all provider/model definitions are centralized there
@@ -110,8 +110,8 @@ import { TIER_COLOR } from '../src/tier-colors.js'
110
110
  import { resolveCloudflareUrl, buildPingRequest, ping, extractQuotaPercent, getProviderQuotaPercentCached, usagePlaceholderForProvider } from '../src/ping.js'
111
111
  import { runFiableMode, filterByTierOrExit, fetchOpenRouterFreeModels } from '../src/analysis.js'
112
112
  import { PROVIDER_METADATA, ENV_VAR_NAMES, isWindows, isMac } from '../src/provider-metadata.js'
113
- import { parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelemetryConfig, getTelemetryDistinctId, getTelemetrySystem, getTelemetryTerminal, isTelemetryEnabled, sendUsageTelemetry, sendBugReport } from '../src/telemetry.js'
114
- import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel } from '../src/favorites.js'
113
+ import { parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelemetryConfig, getTelemetryDistinctId, getTelemetrySystem, getTelemetryTerminal, isTelemetryEnabled, sendUsageTelemetry } from '../src/telemetry.js'
114
+ import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel, reorderFavorite } from '../src/favorites.js'
115
115
  import { checkForUpdateDetailed, checkForUpdate, runUpdate, promptUpdateNotification, fetchLastReleaseDate } from './updater.js'
116
116
  import { promptApiKey } from '../src/setup.js'
117
117
  import { syncShellEnv, ensureShellRcSource, promptShellEnvMigration, removeShellEnv } from '../src/shell-env.js'
@@ -123,6 +123,7 @@ import { startOpenClaw } from '../src/openclaw.js'
123
123
  import { createOverlayRenderers } from '../src/overlays.js'
124
124
  import { createKeyHandler, createMouseEventHandler } from '../src/key-handler.js'
125
125
  import { createMouseHandler, containsMouseSequence } from '../src/mouse.js'
126
+ import { stopRouterDashboardClient } from '../src/router-dashboard.js'
126
127
  import { getToolModeOrder, getToolMeta } from '../src/tool-metadata.js'
127
128
  import { startExternalTool } from '../src/tool-launchers.js'
128
129
  import { getToolInstallPlan, installToolWithPlan, isToolInstalled } from '../src/tool-bootstrap.js'
@@ -228,20 +229,24 @@ export async function runApp(cliArgs, config) {
228
229
 
229
230
  // 📖 Shell env migration popup for existing users who haven't been asked yet
230
231
  // 📖 Only show when user has keys but shellEnvEnabled is still undefined (never prompted)
231
- if (hasAnyKey && config.settings.shellEnvEnabled === undefined) {
232
+ // 📖 shellEnvPromptSeen flag ensures it only shows ONCE even after adding new keys
233
+ if (hasAnyKey && config.settings.shellEnvEnabled === undefined && config.settings.shellEnvPromptSeen !== true) {
232
234
  const choice = await promptShellEnvMigration(config)
235
+ if (!config.settings) config.settings = {}
236
+ config.settings.shellEnvPromptSeen = true
233
237
  if (choice === 'enable') {
234
- if (!config.settings) config.settings = {}
235
238
  config.settings.shellEnvEnabled = true
236
239
  saveConfig(config)
237
240
  syncShellEnv(config)
238
241
  ensureShellRcSource()
239
242
  } else if (choice === 'never') {
240
- if (!config.settings) config.settings = {}
241
243
  config.settings.shellEnvEnabled = false
242
244
  saveConfig(config)
243
245
  }
244
- // 📖 'skip' (Ctrl+C) now also sets shellEnvEnabled = false — prompt won't reappear
246
+ if (choice === 'skip') {
247
+ config.settings.shellEnvEnabled = false
248
+ saveConfig(config)
249
+ }
245
250
  }
246
251
 
247
252
  // 📖 Default mode: use the last persisted launcher choice when valid,
@@ -430,7 +435,6 @@ export async function runApp(cliArgs, config) {
430
435
  healthFilterMode: 0, // 📖 Index into HEALTH_CYCLE (0=All, then health states)
431
436
  hideUnconfiguredModels: config.settings?.hideUnconfiguredModels === true, // 📖 Hide providers with no configured API key when true.
432
437
  favoritesPinnedAndSticky: config.settings?.favoritesPinnedAndSticky === true, // 📖 false by default: favorites follow normal sort/filter rules until Y enables pinned+sticky mode.
433
- footerHidden: config.settings?.footerHidden === true, // 📖 true = footer is collapsed to a single toggle hint
434
438
  scrollOffset: 0, // 📖 First visible model index in viewport
435
439
  terminalRows: process.stdout.rows || 24, // 📖 Current terminal height
436
440
  terminalCols: process.stdout.columns || 80, // 📖 Current terminal width
@@ -503,11 +507,7 @@ export async function runApp(cliArgs, config) {
503
507
  recommendAnalysisTimer: null, // 📖 setInterval handle for the 10s analysis phase
504
508
  recommendPingTimer: null, // 📖 setInterval handle for 2 pings/sec during analysis
505
509
  recommendedKeys: new Set(), // 📖 Set of "providerKey/modelId" for recommended models (shown in main table)
506
- // 📖 Feedback state (J/I keys open it)
507
- feedbackOpen: false, // 📖 Whether the feedback overlay is active
508
- bugReportBuffer: '', // 📖 Typed characters for the feedback message
509
- bugReportStatus: 'idle', // 📖 'idle'|'sending'|'success'|'error' — webhook send status
510
- bugReportError: null, // 📖 Last webhook error message
510
+
511
511
  // 📖 OpenCode sync status (S key in settings)
512
512
  settingsSyncStatus: null, // 📖 { type: 'success'|'error', msg: string } — shown in settings footer
513
513
  // 📖 Changelog overlay state (N key opens it)
@@ -522,6 +522,28 @@ export async function runApp(cliArgs, config) {
522
522
  installedModelsScrollOffset: 0, // 📖 Vertical scroll offset for overlay viewport
523
523
  installedModelsData: [], // 📖 Cached scan results
524
524
  installedModelsErrorMsg: null, // 📖 Error or status message
525
+ // 📖 Router Dashboard overlay state (Shift+R opens it).
526
+ routerDashboardOpen: false,
527
+ routerDashboardStatus: 'idle', // 📖 idle | loading | ready | partial | stopped | stale | unreachable | malformed
528
+ routerDashboardBaseUrl: null,
529
+ routerDashboardPort: null,
530
+ routerDashboardHealth: null,
531
+ routerDashboardStats: null,
532
+ routerDashboardError: null,
533
+ routerDashboardScrollOffset: 0,
534
+ routerDashboardEvents: [],
535
+ routerDashboardLiveRequests: [],
536
+ routerDashboardClearedAt: 0,
537
+ routerDashboardLastUpdatedAt: null,
538
+ routerDashboardLastRefreshStartedAt: null,
539
+ routerDashboardPollTimer: null,
540
+ routerDashboardEventAbort: null,
541
+ routerDashboardEventStatus: 'idle',
542
+ routerDashboardEventError: null,
543
+ routerDashboardNotice: null,
544
+ routerDashboardNoticeTimer: null,
545
+ routerOnboardingScrollOffset: 0,
546
+ routerDashboardEverOpened: false, // 📖 Set to true the first time dashboard opens (used for upgrade-path telemetry)
525
547
  // 📖 Custom text filter (Ctrl+P palette → type text → Enter). Ephemeral — not saved to config.
526
548
  customTextFilter: null, // 📖 Active free-text filter string (null = off). Matches model name, ctx, provider key/name.
527
549
  }
@@ -725,6 +747,7 @@ export async function runApp(cliArgs, config) {
725
747
  clearInterval(ticker)
726
748
  clearTimeout(state.pingIntervalObj)
727
749
  clearInterval(state.versionRecheckTimer)
750
+ stopRouterDashboardClient(state)
728
751
  process.stdout.write(ALT_LEAVE)
729
752
  if (process.stdout.isTTY) {
730
753
  process.stdout.flush && process.stdout.flush()
@@ -798,6 +821,7 @@ export async function runApp(cliArgs, config) {
798
821
  if (ticker) clearInterval(ticker)
799
822
  clearTimeout(state.pingIntervalObj)
800
823
  clearInterval(state.versionRecheckTimer)
824
+ stopRouterDashboardClient(state)
801
825
  if (onKeyPress) process.stdin.removeListener('keypress', onKeyPress)
802
826
  if (onMouseData) process.stdin.removeListener('data', onMouseData)
803
827
  if (process.stdin.isTTY && resetRawMode) process.stdin.setRawMode(false)
@@ -864,6 +888,7 @@ export async function runApp(cliArgs, config) {
864
888
  installProviderEndpoints,
865
889
  syncFavoriteFlags,
866
890
  toggleFavoriteModel,
891
+ reorderFavorite,
867
892
  sortResultsWithPinnedFavorites,
868
893
  adjustScrollOffset,
869
894
  applyTierFilter,
@@ -887,7 +912,6 @@ export async function runApp(cliArgs, config) {
887
912
  sendUsageTelemetry,
888
913
  startRecommendAnalysis: overlays.startRecommendAnalysis,
889
914
  stopRecommendAnalysis: overlays.stopRecommendAnalysis,
890
- sendBugReport,
891
915
  stopUi,
892
916
  ping,
893
917
  TASK_TYPES,
@@ -991,7 +1015,7 @@ export async function runApp(cliArgs, config) {
991
1015
  process.stdout.write(ALT_LEAVE);
992
1016
  console.error(chalk.red('\n[TUI Error] An error occurred while handling a keypress.'));
993
1017
  console.error(err);
994
- console.error(chalk.yellow('\nPlease file an issue at https://github.com/vava-nessa/free-coding-models/issues or use the feedback form (I key) to report this to the author.'));
1018
+ console.error(chalk.yellow('\nPlease file an issue at https://github.com/vava-nessa/free-coding-models/issues or join the Discord to report this to the author.'));
995
1019
  process.exit(1);
996
1020
  }
997
1021
  })
@@ -1015,12 +1039,14 @@ export async function runApp(cliArgs, config) {
1015
1039
  refreshAutoPingMode()
1016
1040
  state.frame++
1017
1041
  // 📖 Cache visible+sorted models each frame so Enter handler always matches the display
1018
- if (!state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.incompatibleFallbackOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen && !state.installedModelsOpen && !state.commandPaletteOpen) {
1042
+ if (!state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.incompatibleFallbackOpen && !state.recommendOpen && !state.changelogOpen && !state.installedModelsOpen && !state.routerDashboardOpen && !state.commandPaletteOpen) {
1019
1043
  const visible = state.results.filter(r => !r.hidden)
1020
1044
  state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
1021
1045
  pinFavorites: state.favoritesPinnedAndSticky,
1022
1046
  })
1023
1047
  }
1048
+ const tableTerminalRows = state.terminalRows
1049
+
1024
1050
  let tableContent = null
1025
1051
  if (state.commandPaletteOpen) {
1026
1052
  if (!state.commandPaletteFrozenTable) {
@@ -1038,7 +1064,7 @@ export async function runApp(cliArgs, config) {
1038
1064
  state.mode,
1039
1065
  state.tierFilterMode,
1040
1066
  state.scrollOffset,
1041
- state.terminalRows,
1067
+ tableTerminalRows,
1042
1068
  state.terminalCols,
1043
1069
  state.originFilterMode,
1044
1070
  null,
@@ -1056,7 +1082,7 @@ export async function runApp(cliArgs, config) {
1056
1082
  state.favoritesPinnedAndSticky,
1057
1083
  state.customTextFilter,
1058
1084
  state.lastReleaseDate,
1059
- state.footerHidden,
1085
+ false,
1060
1086
  state.verdictFilterMode,
1061
1087
  state.healthFilterMode
1062
1088
  )
@@ -1076,7 +1102,7 @@ export async function runApp(cliArgs, config) {
1076
1102
  state.mode,
1077
1103
  state.tierFilterMode,
1078
1104
  state.scrollOffset,
1079
- state.terminalRows,
1105
+ tableTerminalRows,
1080
1106
  state.terminalCols,
1081
1107
  state.originFilterMode,
1082
1108
  null,
@@ -1094,7 +1120,7 @@ export async function runApp(cliArgs, config) {
1094
1120
  state.favoritesPinnedAndSticky,
1095
1121
  state.customTextFilter,
1096
1122
  state.lastReleaseDate,
1097
- state.footerHidden,
1123
+ false,
1098
1124
  state.verdictFilterMode,
1099
1125
  state.healthFilterMode
1100
1126
  )
@@ -1108,15 +1134,19 @@ export async function runApp(cliArgs, config) {
1108
1134
  ? overlays.renderToolInstallPrompt()
1109
1135
  : state.installedModelsOpen
1110
1136
  ? overlays.renderInstalledModels()
1137
+ : state.routerDashboardOpen
1138
+ ? overlays.renderRouterDashboard()
1139
+ : state.tokenUsageOpen
1140
+ ? overlays.renderTokenUsage()
1141
+ : state.routerOnboardingOpen
1142
+ ? overlays.renderRouterOnboarding()
1111
1143
  : state.incompatibleFallbackOpen
1112
1144
  ? overlays.renderIncompatibleFallback()
1113
1145
  : state.commandPaletteOpen
1114
1146
  ? tableContent + overlays.renderCommandPalette()
1115
1147
  : state.recommendOpen
1116
1148
  ? overlays.renderRecommend()
1117
- : state.feedbackOpen
1118
- ? overlays.renderFeedback()
1119
- : state.helpVisible
1149
+ : state.helpVisible
1120
1150
  ? overlays.renderHelp()
1121
1151
  : state.changelogOpen
1122
1152
  ? overlays.renderChangelog()
@@ -1129,7 +1159,7 @@ export async function runApp(cliArgs, config) {
1129
1159
  process.stdout.write(ALT_LEAVE);
1130
1160
  console.error(chalk.red('\n[TUI Render Error] An error occurred during UI rendering.'));
1131
1161
  console.error(err);
1132
- console.error(chalk.yellow('\nPlease file an issue at https://github.com/vava-nessa/free-coding-models/issues or use the feedback form (I key) to report this to the author.'));
1162
+ console.error(chalk.yellow('\nPlease file an issue at https://github.com/vava-nessa/free-coding-models/issues or join the Discord to report this to the author.'));
1133
1163
  process.exit(1);
1134
1164
  }
1135
1165
  }, Math.round(1000 / FPS))
@@ -1140,7 +1170,7 @@ export async function runApp(cliArgs, config) {
1140
1170
  pinFavorites: state.favoritesPinnedAndSticky,
1141
1171
  })
1142
1172
 
1143
- process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate, state.footerHidden, state.verdictFilterMode, state.healthFilterMode))
1173
+ process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate, false, state.verdictFilterMode, state.healthFilterMode))
1144
1174
  if (process.stdout.isTTY) {
1145
1175
  process.stdout.flush && process.stdout.flush()
1146
1176
  }
@@ -1201,7 +1231,7 @@ export async function runApp(cliArgs, config) {
1201
1231
  process.stdout.write(ALT_LEAVE);
1202
1232
  console.error(chalk.red('\n[TUI Error] An error occurred in the ping loop.'));
1203
1233
  console.error(err);
1204
- console.error(chalk.yellow('\nPlease file an issue at https://github.com/vava-nessa/free-coding-models/issues or use the feedback form (I key) to report this to the author.'));
1234
+ console.error(chalk.yellow('\nPlease file an issue at https://github.com/vava-nessa/free-coding-models/issues or join the Discord to report this to the author.'));
1205
1235
  process.exit(1);
1206
1236
  }
1207
1237
  }
@@ -1228,6 +1258,15 @@ export async function runApp(cliArgs, config) {
1228
1258
  } catch {}
1229
1259
  }, VERSION_RECHECK_INTERVAL_MS)
1230
1260
 
1261
+ // 📖 Router ON by default — no onboarding prompt, just auto-enable silently.
1262
+ const routerCfg = state.config?.router
1263
+ if (!routerCfg || routerCfg.onboardingSeen !== true || routerCfg.enabled !== true) {
1264
+ if (!state.config.router) state.config.router = {}
1265
+ state.config.router.enabled = true
1266
+ state.config.router.onboardingSeen = true
1267
+ saveConfig(state.config)
1268
+ }
1269
+
1231
1270
  // 📖 Keep interface running forever - user can select anytime or Ctrl+C to exit
1232
1271
  // 📖 The pings continue running in background with dynamic interval
1233
1272
  // 📖 User can press W to decrease interval (faster pings) or = to increase (slower)
package/src/cache.js CHANGED
@@ -14,7 +14,7 @@
14
14
  * {
15
15
  * "timestamp": 1712345678901, // Last cache write time (ms since epoch)
16
16
  * "models": {
17
- * "nvidia/deepseek-v3.2": {
17
+ * "nvidia/deepseek-ai/deepseek-v4-flash": {
18
18
  * "avg": 245,
19
19
  * "p95": 312,
20
20
  * "jitter": 45,
package/src/cli-help.js CHANGED
@@ -37,6 +37,11 @@ const ANALYSIS_FLAGS = [
37
37
 
38
38
  const CONFIG_FLAGS = [
39
39
  { flag: '--web', description: 'Launch the web dashboard in your browser' },
40
+ { flag: '--daemon', description: 'Start the FCM Router daemon in the foreground' },
41
+ { flag: '--daemon-bg', description: 'Start the FCM Router daemon in the background' },
42
+ { flag: '--daemon-status', description: 'Print FCM Router daemon status JSON' },
43
+ { flag: '--daemon-stop', description: 'Gracefully stop the FCM Router daemon' },
44
+ { flag: '--sync-set [name]', description: 'Auto-discover and live-probe models into a router set' },
40
45
  { flag: '--no-telemetry', description: 'Disable anonymous telemetry for this run' },
41
46
  { flag: '--help, -h', description: 'Print this help and exit' },
42
47
  ]
@@ -44,6 +49,10 @@ const CONFIG_FLAGS = [
44
49
  const EXAMPLES = [
45
50
  'free-coding-models --help',
46
51
  'free-coding-models --web',
52
+ 'free-coding-models --daemon-bg',
53
+ 'free-coding-models --daemon-status',
54
+ 'free-coding-models --sync-set',
55
+ 'free-coding-models --sync-set my-coding-set',
47
56
  'free-coding-models --openclaw --tier S',
48
57
  "free-coding-models --json | jq '.[0]'",
49
58
  ]
@@ -16,6 +16,18 @@
16
16
  */
17
17
 
18
18
  import { TOOL_METADATA, TOOL_MODE_ORDER } from './tool-metadata.js'
19
+ import { sources } from '../sources.js'
20
+
21
+ const PROVIDER_FILTER_COMMANDS = Object.entries(sources).map(([providerKey, source]) => {
22
+ const label = source?.name || providerKey
23
+ return {
24
+ id: `filter-provider-${providerKey.replace(/[^a-z0-9]+/gi, '-')}`,
25
+ label,
26
+ providerKey,
27
+ description: `${label} models`,
28
+ keywords: ['filter', 'provider', 'origin', providerKey, label.toLowerCase()],
29
+ }
30
+ })
19
31
 
20
32
  const TOOL_MODE_DESCRIPTIONS = {
21
33
  opencode: 'Launch in OpenCode CLI with the selected model.',
@@ -119,17 +131,7 @@ const BASE_COMMAND_TREE = [
119
131
  children: [
120
132
  { id: 'filter-provider-cycle', label: 'Cycle provider', shortcut: 'D', description: 'Switch between providers', keywords: ['filter', 'provider', 'origin'] },
121
133
  { id: 'filter-provider-all', label: 'All providers', providerKey: null, description: 'Show all providers', keywords: ['filter', 'provider', 'all'] },
122
- { id: 'filter-provider-nvidia', label: 'NVIDIA NIM', providerKey: 'nvidiaNim', description: 'NVIDIA models', keywords: ['filter', 'provider', 'nvidia', 'nim'] },
123
- { id: 'filter-provider-groq', label: 'Groq', providerKey: 'groq', description: 'Groq models', keywords: ['filter', 'provider', 'groq'] },
124
- { id: 'filter-provider-cerebras', label: 'Cerebras', providerKey: 'cerebras', description: 'Cerebras models', keywords: ['filter', 'provider', 'cerebras'] },
125
- { id: 'filter-provider-sambanova', label: 'SambaNova', providerKey: 'sambanova', description: 'SambaNova models', keywords: ['filter', 'provider', 'sambanova'] },
126
- { id: 'filter-provider-openrouter', label: 'OpenRouter', providerKey: 'openrouter', description: 'OpenRouter models', keywords: ['filter', 'provider', 'openrouter'] },
127
- { id: 'filter-provider-together', label: 'Together AI', providerKey: 'together', description: 'Together models', keywords: ['filter', 'provider', 'together'] },
128
- { id: 'filter-provider-deepinfra', label: 'DeepInfra', providerKey: 'deepinfra', description: 'DeepInfra models', keywords: ['filter', 'provider', 'deepinfra'] },
129
- { id: 'filter-provider-fireworks', label: 'Fireworks AI', providerKey: 'fireworks', description: 'Fireworks models', keywords: ['filter', 'provider', 'fireworks'] },
130
- { id: 'filter-provider-hyperbolic', label: 'Hyperbolic', providerKey: 'hyperbolic', description: 'Hyperbolic models', keywords: ['filter', 'provider', 'hyperbolic'] },
131
- { id: 'filter-provider-google', label: 'Google AI', providerKey: 'google', description: 'Google models', keywords: ['filter', 'provider', 'google'] },
132
- { id: 'filter-provider-huggingface', label: 'Hugging Face', providerKey: 'huggingface', description: 'Hugging Face models', keywords: ['filter', 'provider', 'huggingface'] },
134
+ ...PROVIDER_FILTER_COMMANDS,
133
135
  ]
134
136
  },
135
137
  {
@@ -199,14 +201,14 @@ const BASE_COMMAND_TREE = [
199
201
  ],
200
202
  },
201
203
  { id: 'action-cycle-theme', label: 'Cycle theme', shortcut: 'G', icon: '🌗', description: 'Switch dark/light/auto', keywords: ['theme', 'dark', 'light', 'auto'] },
202
- { id: 'action-reset-view', label: 'Reset view', shortcut: 'Shift+R', icon: '🔄', description: 'Reset filters and sort', keywords: ['reset', 'view', 'sort', 'filters'] },
204
+ { id: 'action-reset-view', label: 'Reset view', icon: '🔄', description: 'Reset filters and sort', keywords: ['reset', 'view', 'sort', 'filters'] },
203
205
  ],
204
206
  },
205
207
  // 📖 Pages - directly at root level, not in submenu
206
208
  { id: 'open-settings', label: 'Settings', shortcut: 'P', icon: '⚙️', type: 'page', description: 'API keys and preferences', keywords: ['settings', 'config', 'api key'] },
207
209
  { id: 'open-help', label: 'Help', shortcut: 'K', icon: '❓', type: 'page', description: 'Show all shortcuts', keywords: ['help', 'shortcuts', 'hotkeys'] },
208
210
  { id: 'open-changelog', label: 'Changelog', shortcut: 'N', icon: '📋', type: 'page', description: 'Version history', keywords: ['changelog', 'release'] },
209
- { id: 'open-feedback', label: 'Feedback', shortcut: 'I', icon: '📝', type: 'page', description: 'Report bugs or requests', keywords: ['feedback', 'bug', 'request'] },
211
+
210
212
  { id: 'open-recommend', label: 'Smart recommend', shortcut: 'Q', icon: '🎯', type: 'page', description: 'Find best model for task', keywords: ['recommend', 'best model'] },
211
213
  { id: 'open-install-endpoints', label: 'Install endpoints', icon: '🔌', type: 'page', description: 'Install provider catalogs', keywords: ['install', 'endpoints', 'providers'] },
212
214
  { id: 'open-installed-models', label: 'Installed models', icon: '🗂️', type: 'page', description: 'View models configured in tools', keywords: ['installed', 'models', 'configured', 'tools', 'manager', 'goose', 'crush', 'aider'] },