free-coding-models 0.1.19 β†’ 0.1.21

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/README.md CHANGED
@@ -56,7 +56,7 @@
56
56
  - **🎨 Clean output** β€” Zero scrollback pollution, interface stays open until Ctrl+C
57
57
  - **πŸ“Ά Status indicators** β€” UP βœ… Β· Timeout ⏳ Β· Overloaded πŸ”₯ Β· Not Found 🚫
58
58
  - **πŸ”§ Multi-source support** β€” Extensible architecture via `sources.js` (add new providers easily)
59
- - **🏷 Tier filtering** β€” Filter models by tier letter (S, A, B, C) with `--tier`
59
+ - **🏷 Tier filtering** β€” Filter models by tier letter (S, A, B, C) with `--tier` flag or dynamically with E/D keys
60
60
 
61
61
  ---
62
62
 
@@ -226,6 +226,15 @@ free-coding-models --tier B # Only B+, B (lightweight options)
226
226
  free-coding-models --tier C # Only C (edge/minimal models)
227
227
  ```
228
228
 
229
+ #### Dynamic tier filtering with E/D keys
230
+
231
+ During runtime, use **E** and **D** keys to dynamically adjust the tier filter:
232
+
233
+ - **E** (Elevate) β€” Show fewer, higher-tier models (cycle: All β†’ S β†’ A β†’ B β†’ C β†’ All)
234
+ - **D** (Descend) β€” Show more, lower-tier models (cycle: All β†’ C β†’ B β†’ A β†’ S β†’ All)
235
+
236
+ Current tier filter is shown in the header badge (e.g., `[Tier S]`)
237
+
229
238
  ---
230
239
 
231
240
  ## πŸ”Œ OpenCode Integration
@@ -439,6 +448,8 @@ This script:
439
448
  - **R/T/O/M/P/A/S/V/U** β€” Sort by Rank/Tier/Origin/Model/Ping/Avg/Status/Verdict/Uptime
440
449
  - **W** β€” Decrease ping interval (faster pings)
441
450
  - **X** β€” Increase ping interval (slower pings)
451
+ - **E** β€” Elevate tier filter (show fewer, higher-tier models)
452
+ - **D** β€” Descend tier filter (show more, lower-tier models)
442
453
  - **Ctrl+C** β€” Exit
443
454
 
444
455
  ---
@@ -327,11 +327,14 @@ const spinCell = (f, o = 0) => chalk.dim.yellow(FRAMES[(f + o) % FRAMES.length].
327
327
  // πŸ“– are imported from lib/utils.js for testability
328
328
 
329
329
  // πŸ“– renderTable: mode param controls footer hint text (opencode vs openclaw)
330
- function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode') {
331
- const up = results.filter(r => r.status === 'up').length
332
- const down = results.filter(r => r.status === 'down').length
333
- const timeout = results.filter(r => r.status === 'timeout').length
334
- const pending = results.filter(r => r.status === 'pending').length
330
+ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilter = null) {
331
+ // πŸ“– Filter out hidden models for display
332
+ const visibleResults = results.filter(r => !r.hidden)
333
+
334
+ const up = visibleResults.filter(r => r.status === 'up').length
335
+ const down = visibleResults.filter(r => r.status === 'down').length
336
+ const timeout = visibleResults.filter(r => r.status === 'timeout').length
337
+ const pending = visibleResults.filter(r => r.status === 'pending').length
335
338
 
336
339
  // πŸ“– Calculate seconds until next ping
337
340
  const timeSinceLastPing = Date.now() - lastPingTime
@@ -354,6 +357,12 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
354
357
  modeBadge = chalk.bold.rgb(0, 200, 255)(' [πŸ’» CLI]')
355
358
  }
356
359
 
360
+ // πŸ“– Tier filter badge shown when filtering is active
361
+ let tierBadge = ''
362
+ if (tierFilter) {
363
+ tierBadge = chalk.bold.rgb(255, 200, 0)(` [Tier ${tierFilter}]`)
364
+ }
365
+
357
366
  // πŸ“– Column widths (generous spacing with margins)
358
367
  const W_RANK = 6
359
368
  const W_TIER = 6
@@ -366,11 +375,11 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
366
375
  const W_UPTIME = 6
367
376
 
368
377
  // πŸ“– Sort models using the shared helper
369
- const sorted = sortResults(results, sortColumn, sortDirection)
378
+ const sorted = sortResults(visibleResults, sortColumn, sortDirection)
370
379
 
371
380
  const lines = [
372
381
  '',
373
- ` ${chalk.bold('⚑ Free Coding Models')} ${chalk.dim('v' + LOCAL_VERSION)}${modeBadge} ` +
382
+ ` ${chalk.bold('⚑ Free Coding Models')} ${chalk.dim('v' + LOCAL_VERSION)}${modeBadge}${tierBadge} ` +
374
383
  chalk.greenBright(`βœ… ${up}`) + chalk.dim(' up ') +
375
384
  chalk.yellow(`⏱ ${timeout}`) + chalk.dim(' timeout ') +
376
385
  chalk.red(`❌ ${down}`) + chalk.dim(' down ') +
@@ -556,7 +565,7 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
556
565
  : mode === 'opencode-desktop'
557
566
  ? chalk.rgb(0, 200, 255)('Enterβ†’OpenDesktop')
558
567
  : chalk.rgb(0, 200, 255)('Enterβ†’OpenCode')
559
- lines.push(chalk.dim(` ↑↓ Navigate β€’ `) + actionHint + chalk.dim(` β€’ R/T/O/M/P/A/S/V/U Sort β€’ W↓/X↑ Interval (${intervalSec}s) β€’ Ctrl+C Exit`))
568
+ lines.push(chalk.dim(` ↑↓ Navigate β€’ `) + actionHint + chalk.dim(` β€’ R/T/O/M/P/A/S/V/U Sort β€’ W↓/X↑ Interval (${intervalSec}s) β€’ E↑/D↓ Tier β€’ Ctrl+C Exit`))
560
569
  lines.push('')
561
570
  lines.push(chalk.dim(' made with ') + '🩷' + chalk.dim(' by vava-nessa β€’ ') + chalk.dim.underline('https://github.com/vava-nessa/free-coding-models'))
562
571
  lines.push('')
@@ -1049,27 +1058,40 @@ async function main() {
1049
1058
  console.log()
1050
1059
  }
1051
1060
 
1052
- // πŸ“– Filter models to only show top tiers if BEST mode is active
1061
+ // πŸ“– Create results array with all models initially visible
1053
1062
  let results = MODELS.map(([modelId, label, tier], i) => ({
1054
1063
  idx: i + 1, modelId, label, tier,
1055
1064
  status: 'pending',
1056
1065
  pings: [], // πŸ“– All ping results (ms or 'TIMEOUT')
1057
1066
  httpCode: null,
1067
+ hidden: false, // πŸ“– Simple flag to hide/show models
1058
1068
  }))
1059
1069
 
1070
+ // πŸ“– Apply filters by setting hidden flag
1060
1071
  if (bestMode) {
1061
- results = results.filter(r => r.tier === 'S+' || r.tier === 'S' || r.tier === 'A+')
1072
+ results.forEach(r => {
1073
+ r.hidden = !(r.tier === 'S+' || r.tier === 'S' || r.tier === 'A+')
1074
+ })
1062
1075
  }
1063
1076
 
1064
- // πŸ“– Apply tier letter filter if --tier X was given
1077
+ // πŸ“– Apply tier letter filter if --tier X was given (just sets initial state)
1078
+ // πŸ“– User can still change filter with E/D keys later
1065
1079
  if (tierFilter) {
1066
- results = filterByTierOrExit(results, tierFilter)
1080
+ const tierSet = TIER_LETTER_MAP[tierFilter]
1081
+ if (!tierSet) {
1082
+ console.error(chalk.red(` βœ– Unknown tier "${tierFilter}". Valid tiers: S, A, B, C`))
1083
+ process.exit(1)
1084
+ }
1085
+ results.forEach(r => {
1086
+ r.hidden = !tierSet.includes(r.tier)
1087
+ })
1067
1088
  }
1068
1089
 
1069
1090
  // πŸ“– Add interactive selection state - cursor index and user's choice
1070
1091
  // πŸ“– sortColumn: 'rank'|'tier'|'origin'|'model'|'ping'|'avg'|'status'|'verdict'|'uptime'
1071
1092
  // πŸ“– sortDirection: 'asc' (default) or 'desc'
1072
1093
  // πŸ“– pingInterval: current interval in ms (default 2000, adjustable with W/X keys)
1094
+ // πŸ“– tierFilter: current tier filter letter (null = all, 'S' = S+/S, 'A' = A+/A/A-, etc.)
1073
1095
  const state = {
1074
1096
  results,
1075
1097
  pendingPings: 0,
@@ -1082,6 +1104,7 @@ async function main() {
1082
1104
  lastPingTime: Date.now(), // πŸ“– Track when last ping cycle started
1083
1105
  fiableMode, // πŸ“– Pass fiable mode to state
1084
1106
  mode, // πŸ“– 'opencode' or 'openclaw' β€” controls Enter action
1107
+ tierFilter: tierFilter || null, // πŸ“– Track current tier filter (null if no --tier flag)
1085
1108
  }
1086
1109
 
1087
1110
  // πŸ“– Enter alternate screen β€” animation runs here, zero scrollback pollution
@@ -1097,6 +1120,19 @@ async function main() {
1097
1120
  process.on('SIGINT', () => exit(0))
1098
1121
  process.on('SIGTERM', () => exit(0))
1099
1122
 
1123
+ // πŸ“– Apply tier filter based on current state.tierFilter
1124
+ // πŸ“– This preserves all ping history by merging with existing results
1125
+ function applyTierFilter() {
1126
+ const tierSet = state.tierFilter ? TIER_LETTER_MAP[state.tierFilter] : null
1127
+
1128
+ // πŸ“– Simple approach: just update hidden flag on existing results
1129
+ state.results.forEach(r => {
1130
+ r.hidden = tierSet ? !tierSet.includes(r.tier) : false
1131
+ })
1132
+
1133
+ return state.results // πŸ“– Return same array, just updated hidden flags
1134
+ }
1135
+
1100
1136
  // πŸ“– Setup keyboard input for interactive selection during pings
1101
1137
  // πŸ“– Use readline with keypress event for arrow key handling
1102
1138
  process.stdin.setEncoding('utf8')
@@ -1129,7 +1165,26 @@ async function main() {
1129
1165
  // πŸ“– Minimum 1s, maximum 60s
1130
1166
  if (key.name === 'w') {
1131
1167
  state.pingInterval = Math.max(1000, state.pingInterval - 1000)
1132
- return
1168
+ } else if (key.name === 'x') {
1169
+ state.pingInterval = Math.min(60000, state.pingInterval + 1000)
1170
+ }
1171
+
1172
+ // πŸ“– Tier filtering keys: E=elevate (more restrictive), D=descend (less restrictive)
1173
+ // πŸ“– Cycle through: null β†’ 'S' β†’ 'A' β†’ 'B' β†’ 'C' β†’ null (all tiers)
1174
+ if (key.name === 'e') {
1175
+ const tierOrder = [null, 'S', 'A', 'B', 'C']
1176
+ const currentIndex = tierOrder.indexOf(state.tierFilter)
1177
+ const nextIndex = (currentIndex + 1) % tierOrder.length
1178
+ state.tierFilter = tierOrder[nextIndex]
1179
+ state.results = applyTierFilter()
1180
+ state.cursor = Math.min(state.cursor, state.results.length - 1)
1181
+ } else if (key.name === 'd') {
1182
+ const tierOrder = [null, 'C', 'B', 'A', 'S']
1183
+ const currentIndex = tierOrder.indexOf(state.tierFilter)
1184
+ const nextIndex = (currentIndex - 1 + tierOrder.length) % tierOrder.length
1185
+ state.tierFilter = tierOrder[nextIndex]
1186
+ state.results = applyTierFilter()
1187
+ state.cursor = Math.min(state.cursor, state.results.length - 1)
1133
1188
  }
1134
1189
 
1135
1190
  if (key.name === 'x') {
@@ -1205,10 +1260,10 @@ async function main() {
1205
1260
  // πŸ“– Animation loop: clear alt screen + redraw table at FPS with cursor
1206
1261
  const ticker = setInterval(() => {
1207
1262
  state.frame++
1208
- process.stdout.write(ALT_CLEAR + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode))
1263
+ process.stdout.write(ALT_CLEAR + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilter))
1209
1264
  }, Math.round(1000 / FPS))
1210
1265
 
1211
- process.stdout.write(ALT_CLEAR + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode))
1266
+ process.stdout.write(ALT_CLEAR + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilter))
1212
1267
 
1213
1268
  // ── Continuous ping loop β€” ping all models every N seconds forever ──────────
1214
1269
 
@@ -1233,14 +1288,14 @@ async function main() {
1233
1288
  }
1234
1289
 
1235
1290
  // πŸ“– Initial ping of all models
1236
- const initialPing = Promise.all(results.map(r => pingModel(r)))
1291
+ const initialPing = Promise.all(state.results.map(r => pingModel(r)))
1237
1292
 
1238
1293
  // πŸ“– Continuous ping loop with dynamic interval (adjustable with W/X keys)
1239
1294
  const schedulePing = () => {
1240
1295
  state.pingIntervalObj = setTimeout(async () => {
1241
1296
  state.lastPingTime = Date.now()
1242
1297
 
1243
- results.forEach(r => {
1298
+ state.results.forEach(r => {
1244
1299
  pingModel(r).catch(() => {
1245
1300
  // Individual ping failures don't crash the loop
1246
1301
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Find the fastest coding LLM models in seconds β€” ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
5
5
  "keywords": [
6
6
  "nvidia",