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.
- package/CHANGELOG.md +40 -0
- package/README.md +112 -1134
- package/bin/free-coding-models.js +34 -188
- package/package.json +2 -3
- package/src/cli-help.js +0 -18
- package/src/config.js +17 -351
- package/src/endpoint-installer.js +26 -64
- package/src/favorites.js +0 -14
- package/src/key-handler.js +74 -641
- package/src/legacy-proxy-cleanup.js +432 -0
- package/src/openclaw.js +69 -108
- package/src/opencode-config.js +48 -0
- package/src/opencode.js +6 -248
- package/src/overlays.js +26 -550
- package/src/product-flags.js +14 -0
- package/src/render-helpers.js +2 -34
- package/src/render-table.js +14 -33
- package/src/testfcm.js +90 -43
- package/src/token-usage-reader.js +9 -38
- package/src/tool-launchers.js +235 -409
- package/src/tool-metadata.js +0 -7
- package/src/utils.js +8 -77
- package/bin/fcm-proxy-daemon.js +0 -242
- package/src/account-manager.js +0 -634
- package/src/anthropic-translator.js +0 -440
- package/src/daemon-manager.js +0 -527
- package/src/error-classifier.js +0 -154
- package/src/log-reader.js +0 -195
- package/src/opencode-sync.js +0 -200
- package/src/proxy-server.js +0 -1477
- package/src/proxy-sync.js +0 -565
- package/src/proxy-topology.js +0 -85
- package/src/request-transformer.js +0 -180
- package/src/responses-translator.js +0 -423
- package/src/token-stats.js +0 -320
|
@@ -80,7 +80,6 @@
|
|
|
80
80
|
* - --json: Output results as JSON (for scripting/automation)
|
|
81
81
|
* - --recommend: Open Smart Recommend immediately on startup
|
|
82
82
|
* - --profile <name>: Load a saved config profile before entering the TUI
|
|
83
|
-
* - --clean-proxy / --proxy-clean: Remove persisted fcm-proxy config from OpenCode
|
|
84
83
|
* - --no-telemetry: Disable anonymous usage analytics for this run
|
|
85
84
|
* - --help / -h: Print the full CLI help and exit
|
|
86
85
|
* - --tier S/A/B/C: Filter models by tier letter (S=S+/S, A=A+/A/A-, B=B+/B, C=C)
|
|
@@ -98,18 +97,15 @@ import { randomUUID } from 'crypto'
|
|
|
98
97
|
import { homedir } from 'os'
|
|
99
98
|
import { join, dirname } from 'path'
|
|
100
99
|
import { MODELS, sources } from '../sources.js'
|
|
101
|
-
import { getAvg, getVerdict, getUptime, getP95, getJitter, getStabilityScore, sortResults, filterByTier, findBestModel, parseArgs, TIER_ORDER, VERDICT_ORDER, TIER_LETTER_MAP, scoreModelForTask, getTopRecommendations, TASK_TYPES, PRIORITY_TYPES, CONTEXT_BUDGETS, formatCtxWindow, labelFromId,
|
|
102
|
-
import { loadConfig, saveConfig, getApiKey,
|
|
100
|
+
import { getAvg, getVerdict, getUptime, getP95, getJitter, getStabilityScore, sortResults, filterByTier, findBestModel, parseArgs, TIER_ORDER, VERDICT_ORDER, TIER_LETTER_MAP, scoreModelForTask, getTopRecommendations, TASK_TYPES, PRIORITY_TYPES, CONTEXT_BUDGETS, formatCtxWindow, labelFromId, formatResultsAsJSON } from '../src/utils.js'
|
|
101
|
+
import { loadConfig, saveConfig, getApiKey, resolveApiKeys, addApiKey, removeApiKey, isProviderEnabled, persistApiKeysForProvider } from '../src/config.js'
|
|
103
102
|
import { buildMergedModels } from '../src/model-merger.js'
|
|
104
|
-
import {
|
|
105
|
-
import { loadOpenCodeConfig, saveOpenCodeConfig, syncToOpenCode, restoreOpenCodeBackup, cleanupOpenCodeProxyConfig } from '../src/opencode-sync.js'
|
|
106
|
-
import { syncProxyToTool, cleanupToolConfig, PROXY_SYNCABLE_TOOLS } from '../src/proxy-sync.js'
|
|
103
|
+
import { loadOpenCodeConfig, saveOpenCodeConfig } from '../src/opencode-config.js'
|
|
107
104
|
import { usageForRow as _usageForRow } from '../src/usage-reader.js'
|
|
108
|
-
import { loadRecentLogs } from '../src/log-reader.js'
|
|
109
105
|
import { buildProviderModelTokenKey, loadTokenUsageByProviderModel } from '../src/token-usage-reader.js'
|
|
110
106
|
import { parseOpenRouterResponse, fetchProviderQuota as _fetchProviderQuotaFromModule } from '../src/provider-quota-fetchers.js'
|
|
111
107
|
import { isKnownQuotaTelemetry } from '../src/quota-capabilities.js'
|
|
112
|
-
import { ALT_ENTER, ALT_LEAVE, ALT_HOME, PING_TIMEOUT, PING_INTERVAL, FPS, COL_MODEL, COL_MS, CELL_W, FRAMES, TIER_CYCLE, SETTINGS_OVERLAY_BG, HELP_OVERLAY_BG, RECOMMEND_OVERLAY_BG,
|
|
108
|
+
import { ALT_ENTER, ALT_LEAVE, ALT_HOME, PING_TIMEOUT, PING_INTERVAL, FPS, COL_MODEL, COL_MS, CELL_W, FRAMES, TIER_CYCLE, SETTINGS_OVERLAY_BG, HELP_OVERLAY_BG, RECOMMEND_OVERLAY_BG, OVERLAY_PANEL_WIDTH, TABLE_HEADER_LINES, TABLE_FOOTER_LINES, TABLE_FIXED_LINES, msCell, spinCell } from '../src/constants.js'
|
|
113
109
|
import { TIER_COLOR } from '../src/tier-colors.js'
|
|
114
110
|
import { resolveCloudflareUrl, buildPingRequest, ping, extractQuotaPercent, getProviderQuotaPercentCached, usagePlaceholderForProvider } from '../src/ping.js'
|
|
115
111
|
import { runFiableMode, filterByTierOrExit, fetchOpenRouterFreeModels } from '../src/analysis.js'
|
|
@@ -118,15 +114,15 @@ import { parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelem
|
|
|
118
114
|
import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel } from '../src/favorites.js'
|
|
119
115
|
import { checkForUpdateDetailed, checkForUpdate, runUpdate, promptUpdateNotification } from '../src/updater.js'
|
|
120
116
|
import { promptApiKey } from '../src/setup.js'
|
|
121
|
-
import { stripAnsi, maskApiKey, displayWidth, padEndDisplay, tintOverlayLines, keepOverlayTargetVisible, sliceOverlayLines, calculateViewport, sortResultsWithPinnedFavorites,
|
|
117
|
+
import { stripAnsi, maskApiKey, displayWidth, padEndDisplay, tintOverlayLines, keepOverlayTargetVisible, sliceOverlayLines, calculateViewport, sortResultsWithPinnedFavorites, adjustScrollOffset } from '../src/render-helpers.js'
|
|
122
118
|
import { renderTable, PROVIDER_COLOR } from '../src/render-table.js'
|
|
123
|
-
import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop
|
|
119
|
+
import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop } from '../src/opencode.js'
|
|
124
120
|
import { startOpenClaw } from '../src/openclaw.js'
|
|
125
121
|
import { createOverlayRenderers } from '../src/overlays.js'
|
|
126
122
|
import { createKeyHandler } from '../src/key-handler.js'
|
|
127
123
|
import { getToolModeOrder, getToolMeta } from '../src/tool-metadata.js'
|
|
128
124
|
import { startExternalTool } from '../src/tool-launchers.js'
|
|
129
|
-
import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels
|
|
125
|
+
import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels } from '../src/endpoint-installer.js'
|
|
130
126
|
import { loadCache, saveCache, clearCache, getCacheAge } from '../src/cache.js'
|
|
131
127
|
import { checkConfigSecurity } from '../src/security.js'
|
|
132
128
|
import { buildCliHelpText } from '../src/cli-help.js'
|
|
@@ -219,115 +215,7 @@ async function main() {
|
|
|
219
215
|
config.settings.sortAsc = true
|
|
220
216
|
}
|
|
221
217
|
|
|
222
|
-
|
|
223
|
-
const cleaned = cleanupOpenCodeProxyConfig()
|
|
224
|
-
console.log()
|
|
225
|
-
console.log(chalk.green(' ✅ OpenCode proxy cleanup complete'))
|
|
226
|
-
console.log(chalk.dim(` Config: ${cleaned.path}`))
|
|
227
|
-
console.log(chalk.dim(` Removed provider: ${cleaned.removedProvider ? 'yes' : 'no'} • Removed default model: ${cleaned.removedModel ? 'yes' : 'no'}`))
|
|
228
|
-
console.log()
|
|
229
|
-
process.exit(0)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// 📖 CLI subcommand: free-coding-models daemon <action>
|
|
233
|
-
const daemonSubcmd = process.argv[2] === 'daemon' ? (process.argv[3] || 'status') : null
|
|
234
|
-
if (daemonSubcmd) {
|
|
235
|
-
const dm = await import('../src/daemon-manager.js')
|
|
236
|
-
if (daemonSubcmd === 'status') {
|
|
237
|
-
const s = await dm.getDaemonStatus()
|
|
238
|
-
console.log()
|
|
239
|
-
if (s.status === 'running') {
|
|
240
|
-
console.log(chalk.greenBright(` 📡 FCM Proxy V2: Running`))
|
|
241
|
-
console.log(chalk.dim(` PID: ${s.info.pid} • Port: ${s.info.port} • Accounts: ${s.info.accountCount} • Version: ${s.info.version}`))
|
|
242
|
-
console.log(chalk.dim(` Started: ${s.info.startedAt}`))
|
|
243
|
-
} else if (s.status === 'stopped') {
|
|
244
|
-
console.log(chalk.yellow(` 📡 FCM Proxy V2: Stopped (service installed but not running)`))
|
|
245
|
-
} else if (s.status === 'stale') {
|
|
246
|
-
console.log(chalk.red(` 📡 FCM Proxy V2: Stale (crashed — PID ${s.info?.pid} no longer alive)`))
|
|
247
|
-
} else if (s.status === 'unhealthy') {
|
|
248
|
-
console.log(chalk.red(` 📡 FCM Proxy V2: Unhealthy (PID alive but health check failed)`))
|
|
249
|
-
} else {
|
|
250
|
-
console.log(chalk.dim(` 📡 FCM Proxy V2: Not installed`))
|
|
251
|
-
console.log(chalk.dim(` Install via: free-coding-models daemon install`))
|
|
252
|
-
}
|
|
253
|
-
console.log()
|
|
254
|
-
process.exit(0)
|
|
255
|
-
}
|
|
256
|
-
if (daemonSubcmd === 'install') {
|
|
257
|
-
const result = dm.installDaemon()
|
|
258
|
-
console.log()
|
|
259
|
-
if (result.success) {
|
|
260
|
-
console.log(chalk.greenBright(' ✅ FCM Proxy V2 background service installed and started!'))
|
|
261
|
-
console.log(chalk.dim(' The proxy will now run automatically at login.'))
|
|
262
|
-
} else {
|
|
263
|
-
console.log(chalk.red(` ❌ Install failed: ${result.error}`))
|
|
264
|
-
}
|
|
265
|
-
console.log()
|
|
266
|
-
process.exit(result.success ? 0 : 1)
|
|
267
|
-
}
|
|
268
|
-
if (daemonSubcmd === 'uninstall') {
|
|
269
|
-
const result = dm.uninstallDaemon()
|
|
270
|
-
console.log()
|
|
271
|
-
if (result.success) {
|
|
272
|
-
console.log(chalk.greenBright(' ✅ FCM Proxy V2 background service uninstalled.'))
|
|
273
|
-
} else {
|
|
274
|
-
console.log(chalk.red(` ❌ Uninstall failed: ${result.error}`))
|
|
275
|
-
}
|
|
276
|
-
console.log()
|
|
277
|
-
process.exit(result.success ? 0 : 1)
|
|
278
|
-
}
|
|
279
|
-
if (daemonSubcmd === 'restart') {
|
|
280
|
-
const result = dm.restartDaemon()
|
|
281
|
-
console.log()
|
|
282
|
-
if (result.success) {
|
|
283
|
-
console.log(chalk.greenBright(' ✅ FCM Proxy V2 service restarted.'))
|
|
284
|
-
} else {
|
|
285
|
-
console.log(chalk.red(` ❌ Restart failed: ${result.error}`))
|
|
286
|
-
}
|
|
287
|
-
console.log()
|
|
288
|
-
process.exit(result.success ? 0 : 1)
|
|
289
|
-
}
|
|
290
|
-
if (daemonSubcmd === 'stop') {
|
|
291
|
-
const result = dm.stopDaemon()
|
|
292
|
-
console.log()
|
|
293
|
-
if (result.success) {
|
|
294
|
-
console.log(chalk.greenBright(' ✅ FCM Proxy V2 service stopped.'))
|
|
295
|
-
console.log(chalk.dim(' The service stays installed and can be restarted later.'))
|
|
296
|
-
} else {
|
|
297
|
-
console.log(chalk.red(` ❌ Stop failed: ${result.error}`))
|
|
298
|
-
}
|
|
299
|
-
console.log()
|
|
300
|
-
process.exit(result.success ? 0 : 1)
|
|
301
|
-
}
|
|
302
|
-
if (daemonSubcmd === 'logs') {
|
|
303
|
-
const logPath = dm.getDaemonLogPath()
|
|
304
|
-
console.log(chalk.dim(` Log file: ${logPath}`))
|
|
305
|
-
try {
|
|
306
|
-
const { execSync } = await import('child_process')
|
|
307
|
-
execSync(`tail -50 "${logPath}"`, { stdio: 'inherit' })
|
|
308
|
-
} catch {
|
|
309
|
-
console.log(chalk.dim(' (no logs yet)'))
|
|
310
|
-
}
|
|
311
|
-
process.exit(0)
|
|
312
|
-
}
|
|
313
|
-
console.log(chalk.red(` Unknown command: ${daemonSubcmd}`))
|
|
314
|
-
console.log(chalk.dim(' Usage: free-coding-models daemon [status|install|uninstall|restart|stop|logs]'))
|
|
315
|
-
process.exit(1)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// 📖 If --profile <name> was passed, load that profile into the live config
|
|
319
|
-
let startupProfileSettings = null
|
|
320
|
-
if (cliArgs.profileName) {
|
|
321
|
-
startupProfileSettings = loadProfile(config, cliArgs.profileName)
|
|
322
|
-
if (!startupProfileSettings) {
|
|
323
|
-
console.error(chalk.red(` Unknown profile "${cliArgs.profileName}". Available: ${listProfiles(config).join(', ') || '(none)'}`))
|
|
324
|
-
process.exit(1)
|
|
325
|
-
}
|
|
326
|
-
saveConfig(config, {
|
|
327
|
-
replaceApiKeys: true,
|
|
328
|
-
replaceFavorites: true,
|
|
329
|
-
})
|
|
330
|
-
}
|
|
218
|
+
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
331
219
|
|
|
332
220
|
// 📖 Check if any provider has a key — if not, run the first-time setup wizard
|
|
333
221
|
const hasAnyKey = Object.keys(sources).some(pk => !!getApiKey(config, pk))
|
|
@@ -343,9 +231,6 @@ async function main() {
|
|
|
343
231
|
}
|
|
344
232
|
}
|
|
345
233
|
|
|
346
|
-
// 📖 Backward-compat: keep apiKey var for startOpenClaw() which still needs it
|
|
347
|
-
let apiKey = getApiKey(config, 'nvidia')
|
|
348
|
-
|
|
349
234
|
// 📖 Default mode: use the last persisted launcher choice when valid,
|
|
350
235
|
// 📖 otherwise fall back to OpenCode CLI.
|
|
351
236
|
let mode = getToolModeOrder().includes(config.settings?.preferredToolMode)
|
|
@@ -359,9 +244,6 @@ async function main() {
|
|
|
359
244
|
aider: cliArgs.aiderMode,
|
|
360
245
|
crush: cliArgs.crushMode,
|
|
361
246
|
goose: cliArgs.gooseMode,
|
|
362
|
-
'claude-code': cliArgs.claudeCodeMode,
|
|
363
|
-
codex: cliArgs.codexMode,
|
|
364
|
-
gemini: cliArgs.geminiMode,
|
|
365
247
|
qwen: cliArgs.qwenMode,
|
|
366
248
|
openhands: cliArgs.openHandsMode,
|
|
367
249
|
amp: cliArgs.ampMode,
|
|
@@ -494,15 +376,15 @@ async function main() {
|
|
|
494
376
|
return 'normal'
|
|
495
377
|
}
|
|
496
378
|
|
|
497
|
-
|
|
379
|
+
// 📖 tierFilter: current tier filter letter (null = all, 'S' = S+/S, 'A' = A+/A/A-, etc.)
|
|
498
380
|
const state = {
|
|
499
381
|
results,
|
|
500
382
|
pendingPings: 0,
|
|
501
383
|
frame: 0,
|
|
502
384
|
cursor: 0,
|
|
503
385
|
selectedModel: null,
|
|
504
|
-
sortColumn:
|
|
505
|
-
sortDirection: (
|
|
386
|
+
sortColumn: config.settings?.sortColumn ?? 'avg',
|
|
387
|
+
sortDirection: (config.settings?.sortAsc ?? true) ? 'asc' : 'desc',
|
|
506
388
|
pingInterval: PING_MODE_INTERVALS.speed, // 📖 Effective live interval derived from the active ping mode.
|
|
507
389
|
pingMode: 'speed', // 📖 Current ping mode: speed | normal | slow | forced.
|
|
508
390
|
pingModeSource: 'startup', // 📖 Why this mode is active: startup | manual | auto | idle | activity.
|
|
@@ -516,7 +398,7 @@ async function main() {
|
|
|
516
398
|
tierFilterMode: 0, // 📖 Index into TIER_CYCLE (0=All, 1=S+, 2=S, ...)
|
|
517
399
|
originFilterMode: 0, // 📖 Index into ORIGIN_CYCLE (0=All, then providers)
|
|
518
400
|
premiumMode: cliArgs.premiumMode, // 📖 Special elite-only mode: S/S+ only, Health UP only, Perfect/Normal/Slow verdict only.
|
|
519
|
-
hideUnconfiguredModels:
|
|
401
|
+
hideUnconfiguredModels: config.settings?.hideUnconfiguredModels === true, // 📖 Hide providers with no configured API key when true.
|
|
520
402
|
disableWidthsWarning: config.settings?.disableWidthsWarning ?? false, // 📖 Disable widths warning toggle (default off)
|
|
521
403
|
scrollOffset: 0, // 📖 First visible model index in viewport
|
|
522
404
|
terminalRows: process.stdout.rows || 24, // 📖 Current terminal height
|
|
@@ -536,15 +418,6 @@ async function main() {
|
|
|
536
418
|
settingsUpdateState: 'idle', // 📖 'idle'|'checking'|'available'|'up-to-date'|'error'|'installing'
|
|
537
419
|
settingsUpdateLatestVersion: null, // 📖 Latest npm version discovered from manual check
|
|
538
420
|
settingsUpdateError: null, // 📖 Last update-check error message for maintenance row
|
|
539
|
-
settingsProxyPortEditMode: false, // 📖 Whether Settings is editing the preferred proxy port field.
|
|
540
|
-
settingsProxyPortBuffer: '', // 📖 Inline input buffer for the preferred proxy port (0 = auto).
|
|
541
|
-
daemonStatus: 'not-installed', // 📖 Background daemon status: 'running'|'stopped'|'stale'|'unhealthy'|'not-installed'
|
|
542
|
-
daemonInfo: null, // 📖 daemon.json contents when daemon is running
|
|
543
|
-
// 📖 Proxy & Daemon overlay state (opened from Settings)
|
|
544
|
-
proxyDaemonOpen: false, // 📖 Whether the dedicated Proxy & Daemon overlay is active
|
|
545
|
-
proxyDaemonCursor: 0, // 📖 Selected row in the proxy/daemon overlay
|
|
546
|
-
proxyDaemonScrollOffset: 0, // 📖 Vertical scroll offset for the proxy/daemon overlay
|
|
547
|
-
proxyDaemonMessage: null, // 📖 Feedback message { type: 'success'|'warning'|'error', msg: string, ts: number }
|
|
548
421
|
config, // 📖 Live reference to the config object (updated on save)
|
|
549
422
|
visibleSorted: [], // 📖 Cached visible+sorted models — shared between render loop and key handlers
|
|
550
423
|
helpVisible: false, // 📖 Whether the help overlay (K key) is active
|
|
@@ -552,12 +425,12 @@ async function main() {
|
|
|
552
425
|
helpScrollOffset: 0, // 📖 Vertical scroll offset for Help overlay viewport
|
|
553
426
|
// 📖 Install Endpoints overlay state (Y key opens it)
|
|
554
427
|
installEndpointsOpen: false, // 📖 Whether the install-endpoints overlay is active
|
|
555
|
-
installEndpointsPhase: 'providers', // 📖 providers | tools |
|
|
428
|
+
installEndpointsPhase: 'providers', // 📖 providers | tools | scope | models | result
|
|
556
429
|
installEndpointsCursor: 0, // 📖 Selected row within the current install phase
|
|
557
430
|
installEndpointsScrollOffset: 0, // 📖 Vertical scroll offset for the install overlay viewport
|
|
558
431
|
installEndpointsProviderKey: null, // 📖 Selected provider for endpoint installation
|
|
559
432
|
installEndpointsToolMode: null, // 📖 Selected target tool mode
|
|
560
|
-
installEndpointsConnectionMode: null, // 📖
|
|
433
|
+
installEndpointsConnectionMode: null, // 📖 Direct provider path retained for future install flow state.
|
|
561
434
|
installEndpointsScope: null, // 📖 all | selected
|
|
562
435
|
installEndpointsSelectedModelIds: new Set(), // 📖 Multi-select buffer for the selected-models phase
|
|
563
436
|
installEndpointsErrorMsg: null, // 📖 Temporary validation/error message inside the install flow
|
|
@@ -574,10 +447,6 @@ async function main() {
|
|
|
574
447
|
recommendAnalysisTimer: null, // 📖 setInterval handle for the 10s analysis phase
|
|
575
448
|
recommendPingTimer: null, // 📖 setInterval handle for 2 pings/sec during analysis
|
|
576
449
|
recommendedKeys: new Set(), // 📖 Set of "providerKey/modelId" for recommended models (shown in main table)
|
|
577
|
-
// 📖 Config Profiles state
|
|
578
|
-
activeProfile: getActiveProfileName(config), // 📖 Currently loaded profile name (or null)
|
|
579
|
-
profileSaveMode: false, // 📖 Whether the inline "Save profile" name input is active
|
|
580
|
-
profileSaveBuffer: '', // 📖 Typed characters for the profile name being saved
|
|
581
450
|
// 📖 Feedback state (J/I keys open it)
|
|
582
451
|
feedbackOpen: false, // 📖 Whether the feedback overlay is active
|
|
583
452
|
bugReportBuffer: '', // 📖 Typed characters for the feedback message
|
|
@@ -585,22 +454,12 @@ async function main() {
|
|
|
585
454
|
bugReportError: null, // 📖 Last webhook error message
|
|
586
455
|
// 📖 OpenCode sync status (S key in settings)
|
|
587
456
|
settingsSyncStatus: null, // 📖 { type: 'success'|'error', msg: string } — shown in settings footer
|
|
588
|
-
// 📖 Log page overlay state (X key opens it)
|
|
589
|
-
logVisible: false, // 📖 Whether the log page overlay is active
|
|
590
|
-
logScrollOffset: 0, // 📖 Vertical scroll offset for log overlay viewport
|
|
591
|
-
logShowAll: false, // 📖 Show all logs (true) or limited to 500 (false)
|
|
592
457
|
// 📖 Changelog overlay state (N key opens it)
|
|
593
458
|
changelogOpen: false, // 📖 Whether the changelog overlay is active
|
|
594
459
|
changelogScrollOffset: 0, // 📖 Vertical scroll offset for changelog overlay viewport
|
|
595
460
|
changelogPhase: 'index', // 📖 'index' (all versions) | 'details' (specific version)
|
|
596
461
|
changelogCursor: 0, // 📖 Selected row in index phase
|
|
597
462
|
changelogSelectedVersion: null, // 📖 Which version to show details for
|
|
598
|
-
// 📖 Proxy startup status — set by autoStartProxyIfSynced, consumed by Task 3 indicator
|
|
599
|
-
// 📖 null = not configured/not attempted
|
|
600
|
-
// 📖 { phase: 'starting' } — proxy start in progress
|
|
601
|
-
// 📖 { phase: 'running', port, accountCount } — proxy is live
|
|
602
|
-
// 📖 { phase: 'failed', reason } — proxy failed to start
|
|
603
|
-
proxyStartupStatus: null, // 📖 Startup-phase proxy status (null | { phase, ...details })
|
|
604
463
|
}
|
|
605
464
|
|
|
606
465
|
// 📖 Re-clamp viewport on terminal resize
|
|
@@ -670,10 +529,6 @@ async function main() {
|
|
|
670
529
|
}
|
|
671
530
|
}
|
|
672
531
|
|
|
673
|
-
// 📖 Auto-start proxy on launch when proxy auto-sync is enabled for the current tool.
|
|
674
|
-
// 📖 Fire-and-forget: does not block UI startup. state.proxyStartupStatus is updated async.
|
|
675
|
-
void autoStartProxyIfSynced(config, state)
|
|
676
|
-
|
|
677
532
|
// 📖 Load cache if available (for faster startup with cached ping results)
|
|
678
533
|
const cached = loadCache()
|
|
679
534
|
if (cached && cached.models) {
|
|
@@ -798,6 +653,9 @@ async function main() {
|
|
|
798
653
|
|
|
799
654
|
// 📖 Enter alternate screen — animation runs here, zero scrollback pollution
|
|
800
655
|
process.stdout.write(ALT_ENTER)
|
|
656
|
+
if (process.stdout.isTTY) {
|
|
657
|
+
process.stdout.flush && process.stdout.flush()
|
|
658
|
+
}
|
|
801
659
|
|
|
802
660
|
// 📖 Ensure we always leave alt screen cleanly (Ctrl+C, crash, normal exit)
|
|
803
661
|
const exit = (code = 0) => {
|
|
@@ -806,6 +664,9 @@ async function main() {
|
|
|
806
664
|
clearInterval(ticker)
|
|
807
665
|
clearTimeout(state.pingIntervalObj)
|
|
808
666
|
process.stdout.write(ALT_LEAVE)
|
|
667
|
+
if (process.stdout.isTTY) {
|
|
668
|
+
process.stdout.flush && process.stdout.flush()
|
|
669
|
+
}
|
|
809
670
|
process.exit(code)
|
|
810
671
|
}
|
|
811
672
|
process.on('SIGINT', () => exit(0))
|
|
@@ -813,7 +674,7 @@ async function main() {
|
|
|
813
674
|
|
|
814
675
|
// 📖 originFilterMode: index into ORIGIN_CYCLE, 0=All, then each provider key in order
|
|
815
676
|
const ORIGIN_CYCLE = [null, ...Object.keys(sources)]
|
|
816
|
-
const resolvedTierFilter =
|
|
677
|
+
const resolvedTierFilter = config.settings?.tierFilter
|
|
817
678
|
state.tierFilterMode = resolvedTierFilter ? Math.max(0, TIER_CYCLE.indexOf(resolvedTierFilter)) : 0
|
|
818
679
|
const resolvedOriginFilter = config.settings?.originFilter
|
|
819
680
|
state.originFilterMode = resolvedOriginFilter ? Math.max(0, ORIGIN_CYCLE.indexOf(resolvedOriginFilter)) : 0
|
|
@@ -865,6 +726,9 @@ async function main() {
|
|
|
865
726
|
if (process.stdin.isTTY && resetRawMode) process.stdin.setRawMode(false)
|
|
866
727
|
process.stdin.pause()
|
|
867
728
|
process.stdout.write(ALT_LEAVE)
|
|
729
|
+
if (process.stdout.isTTY) {
|
|
730
|
+
process.stdout.flush && process.stdout.flush()
|
|
731
|
+
}
|
|
868
732
|
}
|
|
869
733
|
|
|
870
734
|
const overlays = createOverlayRenderers(state, {
|
|
@@ -874,20 +738,16 @@ async function main() {
|
|
|
874
738
|
PROVIDER_COLOR,
|
|
875
739
|
LOCAL_VERSION,
|
|
876
740
|
getApiKey,
|
|
877
|
-
getProxySettings,
|
|
878
741
|
resolveApiKeys,
|
|
879
742
|
isProviderEnabled,
|
|
880
|
-
listProfiles,
|
|
881
743
|
TIER_CYCLE,
|
|
882
744
|
SETTINGS_OVERLAY_BG,
|
|
883
745
|
HELP_OVERLAY_BG,
|
|
884
746
|
RECOMMEND_OVERLAY_BG,
|
|
885
|
-
LOG_OVERLAY_BG,
|
|
886
747
|
OVERLAY_PANEL_WIDTH,
|
|
887
748
|
keepOverlayTargetVisible,
|
|
888
749
|
sliceOverlayLines,
|
|
889
750
|
tintOverlayLines,
|
|
890
|
-
loadRecentLogs,
|
|
891
751
|
TASK_TYPES,
|
|
892
752
|
PRIORITY_TYPES,
|
|
893
753
|
CONTEXT_BUDGETS,
|
|
@@ -902,7 +762,6 @@ async function main() {
|
|
|
902
762
|
getConfiguredInstallableProviders,
|
|
903
763
|
getInstallTargetModes,
|
|
904
764
|
getProviderCatalogModels,
|
|
905
|
-
CONNECTION_MODES,
|
|
906
765
|
getToolMeta,
|
|
907
766
|
})
|
|
908
767
|
|
|
@@ -913,23 +772,16 @@ async function main() {
|
|
|
913
772
|
MODELS,
|
|
914
773
|
sources,
|
|
915
774
|
getApiKey,
|
|
916
|
-
getProxySettings,
|
|
917
775
|
resolveApiKeys,
|
|
918
776
|
addApiKey,
|
|
919
777
|
removeApiKey,
|
|
920
778
|
persistApiKeysForProvider,
|
|
921
779
|
isProviderEnabled,
|
|
922
|
-
listProfiles,
|
|
923
|
-
loadProfile,
|
|
924
|
-
deleteProfile,
|
|
925
|
-
saveAsProfile,
|
|
926
|
-
setActiveProfile,
|
|
927
780
|
saveConfig,
|
|
928
781
|
getConfiguredInstallableProviders,
|
|
929
782
|
getInstallTargetModes,
|
|
930
783
|
getProviderCatalogModels,
|
|
931
784
|
installProviderEndpoints,
|
|
932
|
-
CONNECTION_MODES,
|
|
933
785
|
syncFavoriteFlags,
|
|
934
786
|
toggleFavoriteModel,
|
|
935
787
|
sortResultsWithPinnedFavorites,
|
|
@@ -939,19 +791,12 @@ async function main() {
|
|
|
939
791
|
TIER_CYCLE,
|
|
940
792
|
ORIGIN_CYCLE,
|
|
941
793
|
ENV_VAR_NAMES,
|
|
942
|
-
ensureProxyRunning,
|
|
943
|
-
syncToOpenCode,
|
|
944
|
-
cleanupToolConfig,
|
|
945
|
-
restoreOpenCodeBackup,
|
|
946
794
|
checkForUpdateDetailed,
|
|
947
795
|
runUpdate,
|
|
948
796
|
startOpenClaw,
|
|
949
797
|
startOpenCodeDesktop,
|
|
950
798
|
startOpenCode,
|
|
951
|
-
startProxyAndLaunch,
|
|
952
799
|
startExternalTool,
|
|
953
|
-
buildProxyTopologyFromConfig,
|
|
954
|
-
isProxyEnabledForConfig,
|
|
955
800
|
getToolModeOrder,
|
|
956
801
|
startRecommendAnalysis: overlays.startRecommendAnalysis,
|
|
957
802
|
stopRecommendAnalysis: overlays.stopRecommendAnalysis,
|
|
@@ -963,7 +808,6 @@ async function main() {
|
|
|
963
808
|
CONTEXT_BUDGETS,
|
|
964
809
|
toFavoriteKey,
|
|
965
810
|
mergedModels,
|
|
966
|
-
apiKey,
|
|
967
811
|
chalk,
|
|
968
812
|
setPingMode,
|
|
969
813
|
noteUserActivity,
|
|
@@ -1002,14 +846,12 @@ async function main() {
|
|
|
1002
846
|
refreshAutoPingMode()
|
|
1003
847
|
state.frame++
|
|
1004
848
|
// 📖 Cache visible+sorted models each frame so Enter handler always matches the display
|
|
1005
|
-
if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen
|
|
849
|
+
if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen) {
|
|
1006
850
|
const visible = state.results.filter(r => !r.hidden)
|
|
1007
851
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1008
852
|
}
|
|
1009
853
|
const content = state.settingsOpen
|
|
1010
854
|
? overlays.renderSettings()
|
|
1011
|
-
: state.proxyDaemonOpen
|
|
1012
|
-
? overlays.renderProxyDaemon()
|
|
1013
855
|
: state.installEndpointsOpen
|
|
1014
856
|
? overlays.renderInstallEndpoints()
|
|
1015
857
|
: state.recommendOpen
|
|
@@ -1018,19 +860,23 @@ async function main() {
|
|
|
1018
860
|
? overlays.renderFeedback()
|
|
1019
861
|
: state.helpVisible
|
|
1020
862
|
? overlays.renderHelp()
|
|
1021
|
-
: state.logVisible
|
|
1022
|
-
? overlays.renderLog()
|
|
1023
863
|
: state.changelogOpen
|
|
1024
864
|
? overlays.renderChangelog()
|
|
1025
|
-
|
|
865
|
+
: 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)
|
|
1026
866
|
process.stdout.write(ALT_HOME + content)
|
|
867
|
+
if (process.stdout.isTTY) {
|
|
868
|
+
process.stdout.flush && process.stdout.flush()
|
|
869
|
+
}
|
|
1027
870
|
}, Math.round(1000 / FPS))
|
|
1028
871
|
|
|
1029
872
|
// 📖 Populate visibleSorted before the first frame so Enter works immediately
|
|
1030
873
|
const initialVisible = state.results.filter(r => !r.hidden)
|
|
1031
874
|
state.visibleSorted = sortResultsWithPinnedFavorites(initialVisible, state.sortColumn, state.sortDirection)
|
|
1032
875
|
|
|
1033
|
-
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,
|
|
876
|
+
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))
|
|
877
|
+
if (process.stdout.isTTY) {
|
|
878
|
+
process.stdout.flush && process.stdout.flush()
|
|
879
|
+
}
|
|
1034
880
|
|
|
1035
881
|
// 📖 If --recommend was passed, auto-open the Smart Recommend overlay on start
|
|
1036
882
|
if (cliArgs.recommendMode) {
|
|
@@ -1054,7 +900,7 @@ async function main() {
|
|
|
1054
900
|
refreshAutoPingMode()
|
|
1055
901
|
state.lastPingTime = Date.now()
|
|
1056
902
|
|
|
1057
|
-
// 📖 Refresh persisted usage snapshots each cycle so
|
|
903
|
+
// 📖 Refresh persisted usage snapshots each cycle so background usage data appears live in table.
|
|
1058
904
|
// 📖 Freshness-aware: stale snapshots (>30m) are excluded and row reverts to undefined.
|
|
1059
905
|
for (const r of state.results) {
|
|
1060
906
|
const pct = _usageForRow(r.providerKey, r.modelId)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
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",
|
|
@@ -36,8 +36,7 @@
|
|
|
36
36
|
"type": "module",
|
|
37
37
|
"main": "bin/free-coding-models.js",
|
|
38
38
|
"bin": {
|
|
39
|
-
"free-coding-models": "./bin/free-coding-models.js"
|
|
40
|
-
"fcm-proxy-daemon": "./bin/fcm-proxy-daemon.js"
|
|
39
|
+
"free-coding-models": "./bin/free-coding-models.js"
|
|
41
40
|
},
|
|
42
41
|
"files": [
|
|
43
42
|
"bin/",
|
package/src/cli-help.js
CHANGED
|
@@ -37,26 +37,14 @@ const ANALYSIS_FLAGS = [
|
|
|
37
37
|
]
|
|
38
38
|
|
|
39
39
|
const CONFIG_FLAGS = [
|
|
40
|
-
{ flag: '--profile <name>', description: 'Load a saved config profile before startup' },
|
|
41
40
|
{ flag: '--no-telemetry', description: 'Disable anonymous telemetry for this run' },
|
|
42
|
-
{ flag: '--clean-proxy, --proxy-clean', description: 'Remove persisted fcm-proxy config from OpenCode' },
|
|
43
41
|
{ flag: '--help, -h', description: 'Print this help and exit' },
|
|
44
42
|
]
|
|
45
43
|
|
|
46
|
-
const COMMANDS = [
|
|
47
|
-
{ command: 'daemon status', description: 'Show background FCM Proxy V2 service status' },
|
|
48
|
-
{ command: 'daemon install', description: 'Install and start the background service' },
|
|
49
|
-
{ command: 'daemon uninstall', description: 'Remove the background service' },
|
|
50
|
-
{ command: 'daemon restart', description: 'Restart the background service' },
|
|
51
|
-
{ command: 'daemon stop', description: 'Gracefully stop the background service without uninstalling it' },
|
|
52
|
-
{ command: 'daemon logs', description: 'Print the latest daemon log lines' },
|
|
53
|
-
]
|
|
54
|
-
|
|
55
44
|
const EXAMPLES = [
|
|
56
45
|
'free-coding-models --help',
|
|
57
46
|
'free-coding-models --openclaw --tier S',
|
|
58
47
|
"free-coding-models --json | jq '.[0]'",
|
|
59
|
-
'free-coding-models daemon status',
|
|
60
48
|
]
|
|
61
49
|
|
|
62
50
|
function paint(chalk, formatter, text) {
|
|
@@ -79,7 +67,6 @@ export function buildCliHelpLines({ chalk = null, indent = '', title = 'CLI Help
|
|
|
79
67
|
|
|
80
68
|
lines.push(`${indent}${paint(chalk, chalk?.bold, title)}`)
|
|
81
69
|
lines.push(`${indent}${paint(chalk, chalk?.dim, 'Usage: free-coding-models [apiKey] [options]')}`)
|
|
82
|
-
lines.push(`${indent}${paint(chalk, chalk?.dim, ' free-coding-models daemon [status|install|uninstall|restart|stop|logs]')}`)
|
|
83
70
|
lines.push('')
|
|
84
71
|
lines.push(`${indent}${paint(chalk, chalk?.bold, 'Tool Flags')}`)
|
|
85
72
|
for (const entry of launchFlags) {
|
|
@@ -96,11 +83,6 @@ export function buildCliHelpLines({ chalk = null, indent = '', title = 'CLI Help
|
|
|
96
83
|
lines.push(formatEntry(entry.flag, entry.description, { chalk, indent }))
|
|
97
84
|
}
|
|
98
85
|
lines.push('')
|
|
99
|
-
lines.push(`${indent}${paint(chalk, chalk?.bold, 'Commands')}`)
|
|
100
|
-
for (const entry of COMMANDS) {
|
|
101
|
-
lines.push(formatEntry(entry.command, entry.description, { chalk, indent }))
|
|
102
|
-
}
|
|
103
|
-
lines.push('')
|
|
104
86
|
lines.push(`${indent}${paint(chalk, chalk?.dim, 'Default launcher with no tool flag: OpenCode CLI')}`)
|
|
105
87
|
lines.push(`${indent}${paint(chalk, chalk?.dim, 'Flags can be combined: --openclaw --tier S --json')}`)
|
|
106
88
|
lines.push('')
|