free-coding-models 0.1.23 → 0.1.25
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 +4 -0
- package/bin/free-coding-models.js +122 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -266,10 +266,15 @@ async function promptUpdateNotification(latestVersion) {
|
|
|
266
266
|
// ─── Alternate screen control ─────────────────────────────────────────────────
|
|
267
267
|
// 📖 \x1b[?1049h = enter alt screen \x1b[?1049l = leave alt screen
|
|
268
268
|
// 📖 \x1b[?25l = hide cursor \x1b[?25h = show cursor
|
|
269
|
-
// 📖 \x1b[H = cursor to top
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
269
|
+
// 📖 \x1b[H = cursor to top
|
|
270
|
+
// 📖 NOTE: We avoid \x1b[2J (clear screen) because Ghostty scrolls cleared
|
|
271
|
+
// 📖 content into the scrollback on the alt screen, pushing the header off-screen.
|
|
272
|
+
// 📖 Instead we overwrite in place: cursor home, then \x1b[K (erase to EOL) per line.
|
|
273
|
+
// 📖 \x1b[?7l disables auto-wrap so wide rows clip at the right edge instead of
|
|
274
|
+
// 📖 wrapping to the next line (which would double the row height and overflow).
|
|
275
|
+
const ALT_ENTER = '\x1b[?1049h\x1b[?25l\x1b[?7l'
|
|
276
|
+
const ALT_LEAVE = '\x1b[?7h\x1b[?1049l\x1b[?25h'
|
|
277
|
+
const ALT_HOME = '\x1b[H'
|
|
273
278
|
|
|
274
279
|
// ─── API Configuration ───────────────────────────────────────────────────────────
|
|
275
280
|
// 📖 Models are now loaded from sources.js to support multiple providers
|
|
@@ -321,11 +326,31 @@ const spinCell = (f, o = 0) => chalk.dim.yellow(FRAMES[(f + o) % FRAMES.length].
|
|
|
321
326
|
// 📖 Core logic functions (getAvg, getVerdict, getUptime, sortResults, etc.)
|
|
322
327
|
// 📖 are imported from lib/utils.js for testability
|
|
323
328
|
|
|
329
|
+
// ─── Viewport calculation ────────────────────────────────────────────────────
|
|
330
|
+
// 📖 Computes the visible slice of model rows that fits in the terminal.
|
|
331
|
+
// 📖 Fixed lines: 5 header + 5 footer = 10 lines always consumed.
|
|
332
|
+
// 📖 Header: empty, title, empty, column headers, separator (5)
|
|
333
|
+
// 📖 Footer: empty, hints, empty, credit, empty (5)
|
|
334
|
+
// 📖 When scroll indicators are needed, they each consume 1 line from the model budget.
|
|
335
|
+
function calculateViewport(terminalRows, scrollOffset, totalModels) {
|
|
336
|
+
if (terminalRows <= 0) return { startIdx: 0, endIdx: totalModels, hasAbove: false, hasBelow: false }
|
|
337
|
+
let maxSlots = terminalRows - 10 // 5 header + 5 footer
|
|
338
|
+
if (maxSlots < 1) maxSlots = 1
|
|
339
|
+
if (totalModels <= maxSlots) return { startIdx: 0, endIdx: totalModels, hasAbove: false, hasBelow: false }
|
|
340
|
+
|
|
341
|
+
const hasAbove = scrollOffset > 0
|
|
342
|
+
const hasBelow = scrollOffset + maxSlots - (hasAbove ? 1 : 0) < totalModels
|
|
343
|
+
// Recalculate with indicator lines accounted for
|
|
344
|
+
const modelSlots = maxSlots - (hasAbove ? 1 : 0) - (hasBelow ? 1 : 0)
|
|
345
|
+
const endIdx = Math.min(scrollOffset + modelSlots, totalModels)
|
|
346
|
+
return { startIdx: scrollOffset, endIdx, hasAbove, hasBelow }
|
|
347
|
+
}
|
|
348
|
+
|
|
324
349
|
// 📖 renderTable: mode param controls footer hint text (opencode vs openclaw)
|
|
325
|
-
function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode',
|
|
350
|
+
function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0) {
|
|
326
351
|
// 📖 Filter out hidden models for display
|
|
327
352
|
const visibleResults = results.filter(r => !r.hidden)
|
|
328
|
-
|
|
353
|
+
|
|
329
354
|
const up = visibleResults.filter(r => r.status === 'up').length
|
|
330
355
|
const down = visibleResults.filter(r => r.status === 'down').length
|
|
331
356
|
const timeout = visibleResults.filter(r => r.status === 'timeout').length
|
|
@@ -358,8 +383,9 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
|
|
|
358
383
|
|
|
359
384
|
// 📖 Tier filter badge shown when filtering is active
|
|
360
385
|
let tierBadge = ''
|
|
361
|
-
if (
|
|
362
|
-
|
|
386
|
+
if (tierFilterMode > 0) {
|
|
387
|
+
const tierNames = ['All', 'S+/S', 'A+/A/A-', 'B+/B', 'C']
|
|
388
|
+
tierBadge = chalk.bold.rgb(255, 200, 0)(` [${tierNames[tierFilterMode]}]`)
|
|
363
389
|
}
|
|
364
390
|
|
|
365
391
|
// 📖 Column widths (generous spacing with margins)
|
|
@@ -429,7 +455,14 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
|
|
|
429
455
|
chalk.dim('─'.repeat(W_UPTIME))
|
|
430
456
|
)
|
|
431
457
|
|
|
432
|
-
|
|
458
|
+
// 📖 Viewport clipping: only render models that fit on screen
|
|
459
|
+
const vp = calculateViewport(terminalRows, scrollOffset, sorted.length)
|
|
460
|
+
|
|
461
|
+
if (vp.hasAbove) {
|
|
462
|
+
lines.push(chalk.dim(` ... ${vp.startIdx} more above ...`))
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
for (let i = vp.startIdx; i < vp.endIdx; i++) {
|
|
433
466
|
const r = sorted[i]
|
|
434
467
|
const tierFn = TIER_COLOR[r.tier] ?? (t => chalk.white(t))
|
|
435
468
|
|
|
@@ -555,6 +588,10 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
|
|
|
555
588
|
}
|
|
556
589
|
}
|
|
557
590
|
|
|
591
|
+
if (vp.hasBelow) {
|
|
592
|
+
lines.push(chalk.dim(` ... ${sorted.length - vp.endIdx} more below ...`))
|
|
593
|
+
}
|
|
594
|
+
|
|
558
595
|
lines.push('')
|
|
559
596
|
const intervalSec = Math.round(pingInterval / 1000)
|
|
560
597
|
|
|
@@ -564,11 +601,20 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
|
|
|
564
601
|
: mode === 'opencode-desktop'
|
|
565
602
|
? chalk.rgb(0, 200, 255)('Enter→OpenDesktop')
|
|
566
603
|
: chalk.rgb(0, 200, 255)('Enter→OpenCode')
|
|
567
|
-
lines.push(chalk.dim(` ↑↓ Navigate • `) + actionHint + chalk.dim(` • R/T/O/M/P/A/S/V/U Sort • W↓/X↑ Interval (${intervalSec}s) •
|
|
604
|
+
lines.push(chalk.dim(` ↑↓ Navigate • `) + actionHint + chalk.dim(` • R/T/O/M/P/A/S/V/U Sort • W↓/X↑ Interval (${intervalSec}s) • T Tier • Z Mode • Ctrl+C Exit`))
|
|
568
605
|
lines.push('')
|
|
569
|
-
lines.push(chalk.dim('
|
|
606
|
+
lines.push(chalk.dim(' Made with love by ') + '\x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\')
|
|
607
|
+
lines.push(chalk.dim(' 📂 Repository GitHub: ') + chalk.dim.underline('https://github.com/vava-nessa/free-coding-models'))
|
|
608
|
+
lines.push(chalk.dim(' 💬 Contribuer et discuter sur notre Discord: ') + chalk.dim.underline('https://discord.gg/U4vz7mYQ'))
|
|
570
609
|
lines.push('')
|
|
571
|
-
|
|
610
|
+
// 📖 Append \x1b[K (erase to EOL) to each line so leftover chars from previous
|
|
611
|
+
// 📖 frames are cleared. Then pad with blank cleared lines to fill the terminal,
|
|
612
|
+
// 📖 preventing stale content from lingering at the bottom after resize.
|
|
613
|
+
const EL = '\x1b[K'
|
|
614
|
+
const cleared = lines.map(l => l + EL)
|
|
615
|
+
const remaining = terminalRows > 0 ? Math.max(0, terminalRows - cleared.length) : 0
|
|
616
|
+
for (let i = 0; i < remaining; i++) cleared.push(EL)
|
|
617
|
+
return cleared.join('\n')
|
|
572
618
|
}
|
|
573
619
|
|
|
574
620
|
// ─── HTTP ping ────────────────────────────────────────────────────────────────
|
|
@@ -1030,6 +1076,31 @@ async function main() {
|
|
|
1030
1076
|
|
|
1031
1077
|
// 📖 No initial filters - all models visible by default
|
|
1032
1078
|
|
|
1079
|
+
// 📖 Clamp scrollOffset so cursor is always within the visible viewport window.
|
|
1080
|
+
// 📖 Called after every cursor move, sort change, and terminal resize.
|
|
1081
|
+
const adjustScrollOffset = (st) => {
|
|
1082
|
+
const total = st.results.length
|
|
1083
|
+
let maxSlots = st.terminalRows - 10 // 5 header + 5 footer
|
|
1084
|
+
if (maxSlots < 1) maxSlots = 1
|
|
1085
|
+
if (total <= maxSlots) { st.scrollOffset = 0; return }
|
|
1086
|
+
// Ensure cursor is not above the visible window
|
|
1087
|
+
if (st.cursor < st.scrollOffset) {
|
|
1088
|
+
st.scrollOffset = st.cursor
|
|
1089
|
+
}
|
|
1090
|
+
// Ensure cursor is not below the visible window
|
|
1091
|
+
// Account for indicator lines eating into model slots
|
|
1092
|
+
const hasAbove = st.scrollOffset > 0
|
|
1093
|
+
const tentativeBelow = st.scrollOffset + maxSlots - (hasAbove ? 1 : 0) < total
|
|
1094
|
+
const modelSlots = maxSlots - (hasAbove ? 1 : 0) - (tentativeBelow ? 1 : 0)
|
|
1095
|
+
if (st.cursor >= st.scrollOffset + modelSlots) {
|
|
1096
|
+
st.scrollOffset = st.cursor - modelSlots + 1
|
|
1097
|
+
}
|
|
1098
|
+
// Final clamp
|
|
1099
|
+
const maxOffset = Math.max(0, total - maxSlots)
|
|
1100
|
+
if (st.scrollOffset > maxOffset) st.scrollOffset = maxOffset
|
|
1101
|
+
if (st.scrollOffset < 0) st.scrollOffset = 0
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1033
1104
|
// 📖 Add interactive selection state - cursor index and user's choice
|
|
1034
1105
|
// 📖 sortColumn: 'rank'|'tier'|'origin'|'model'|'ping'|'avg'|'status'|'verdict'|'uptime'
|
|
1035
1106
|
// 📖 sortDirection: 'asc' (default) or 'desc'
|
|
@@ -1046,8 +1117,16 @@ async function main() {
|
|
|
1046
1117
|
pingInterval: PING_INTERVAL, // 📖 Track current interval for W/X keys
|
|
1047
1118
|
lastPingTime: Date.now(), // 📖 Track when last ping cycle started
|
|
1048
1119
|
mode, // 📖 'opencode' or 'openclaw' — controls Enter action
|
|
1120
|
+
scrollOffset: 0, // 📖 First visible model index in viewport
|
|
1121
|
+
terminalRows: process.stdout.rows || 24, // 📖 Current terminal height
|
|
1049
1122
|
}
|
|
1050
1123
|
|
|
1124
|
+
// 📖 Re-clamp viewport on terminal resize
|
|
1125
|
+
process.stdout.on('resize', () => {
|
|
1126
|
+
state.terminalRows = process.stdout.rows || 24
|
|
1127
|
+
adjustScrollOffset(state)
|
|
1128
|
+
})
|
|
1129
|
+
|
|
1051
1130
|
// 📖 Enter alternate screen — animation runs here, zero scrollback pollution
|
|
1052
1131
|
process.stdout.write(ALT_ENTER)
|
|
1053
1132
|
|
|
@@ -1061,11 +1140,27 @@ async function main() {
|
|
|
1061
1140
|
process.on('SIGINT', () => exit(0))
|
|
1062
1141
|
process.on('SIGTERM', () => exit(0))
|
|
1063
1142
|
|
|
1064
|
-
// 📖
|
|
1143
|
+
// 📖 Tier filtering system - cycles through filter modes
|
|
1144
|
+
let tierFilterMode = 0 // 0=all, 1=S+/S, 2=A+/A/A-, 3=B+/B, 4=C
|
|
1065
1145
|
function applyTierFilter() {
|
|
1066
|
-
// 📖 All models visible by default
|
|
1067
1146
|
state.results.forEach(r => {
|
|
1068
|
-
|
|
1147
|
+
switch (tierFilterMode) {
|
|
1148
|
+
case 0: // All tiers visible
|
|
1149
|
+
r.hidden = false
|
|
1150
|
+
break
|
|
1151
|
+
case 1: // S+ and S only
|
|
1152
|
+
r.hidden = !(r.tier === 'S+' || r.tier === 'S')
|
|
1153
|
+
break
|
|
1154
|
+
case 2: // A+, A, A- only
|
|
1155
|
+
r.hidden = !(r.tier === 'A+' || r.tier === 'A' || r.tier === 'A-')
|
|
1156
|
+
break
|
|
1157
|
+
case 3: // B+ and B only
|
|
1158
|
+
r.hidden = !(r.tier === 'B+' || r.tier === 'B')
|
|
1159
|
+
break
|
|
1160
|
+
case 4: // C only
|
|
1161
|
+
r.hidden = r.tier !== 'C'
|
|
1162
|
+
break
|
|
1163
|
+
}
|
|
1069
1164
|
})
|
|
1070
1165
|
|
|
1071
1166
|
return state.results
|
|
@@ -1096,6 +1191,7 @@ async function main() {
|
|
|
1096
1191
|
state.sortColumn = col
|
|
1097
1192
|
state.sortDirection = 'asc'
|
|
1098
1193
|
}
|
|
1194
|
+
adjustScrollOffset(state)
|
|
1099
1195
|
return
|
|
1100
1196
|
}
|
|
1101
1197
|
|
|
@@ -1107,7 +1203,13 @@ async function main() {
|
|
|
1107
1203
|
state.pingInterval = Math.min(60000, state.pingInterval + 1000)
|
|
1108
1204
|
}
|
|
1109
1205
|
|
|
1110
|
-
// 📖 Tier
|
|
1206
|
+
// 📖 Tier toggle key: T = cycle through tier filters (all → S+/S → A+/A/A- → B+/B → C → all)
|
|
1207
|
+
if (key.name === 't') {
|
|
1208
|
+
tierFilterMode = (tierFilterMode + 1) % 5
|
|
1209
|
+
applyTierFilter()
|
|
1210
|
+
adjustScrollOffset(state)
|
|
1211
|
+
return
|
|
1212
|
+
}
|
|
1111
1213
|
|
|
1112
1214
|
// 📖 Mode toggle key: Z = cycle through modes (CLI → Desktop → OpenClaw)
|
|
1113
1215
|
if (key.name === 'z') {
|
|
@@ -1126,6 +1228,7 @@ async function main() {
|
|
|
1126
1228
|
if (key.name === 'up') {
|
|
1127
1229
|
if (state.cursor > 0) {
|
|
1128
1230
|
state.cursor--
|
|
1231
|
+
adjustScrollOffset(state)
|
|
1129
1232
|
}
|
|
1130
1233
|
return
|
|
1131
1234
|
}
|
|
@@ -1133,6 +1236,7 @@ async function main() {
|
|
|
1133
1236
|
if (key.name === 'down') {
|
|
1134
1237
|
if (state.cursor < results.length - 1) {
|
|
1135
1238
|
state.cursor++
|
|
1239
|
+
adjustScrollOffset(state)
|
|
1136
1240
|
}
|
|
1137
1241
|
return
|
|
1138
1242
|
}
|
|
@@ -1191,10 +1295,10 @@ async function main() {
|
|
|
1191
1295
|
// 📖 Animation loop: clear alt screen + redraw table at FPS with cursor
|
|
1192
1296
|
const ticker = setInterval(() => {
|
|
1193
1297
|
state.frame++
|
|
1194
|
-
|
|
1298
|
+
process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, tierFilterMode, state.scrollOffset, state.terminalRows))
|
|
1195
1299
|
}, Math.round(1000 / FPS))
|
|
1196
1300
|
|
|
1197
|
-
process.stdout.write(
|
|
1301
|
+
process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, tierFilterMode, state.scrollOffset, state.terminalRows))
|
|
1198
1302
|
|
|
1199
1303
|
// ── Continuous ping loop — ping all models every N seconds forever ──────────
|
|
1200
1304
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
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",
|