free-coding-models 0.1.62 โ 0.1.63
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 +38 -12
- package/bin/free-coding-models.js +399 -14
- package/lib/config.js +17 -2
- package/lib/utils.js +4 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
- **๐ถ Status indicators** โ UP โ
ยท No Key ๐ ยท Timeout โณ ยท Overloaded ๐ฅ ยท Not Found ๐ซ
|
|
65
65
|
- **๐ Keyless latency** โ Models are pinged even without an API key โ a `๐ NO KEY` status confirms the server is reachable with real latency shown, so you can compare providers before committing to a key
|
|
66
66
|
- **๐ท Tier filtering** โ Filter models by tier letter (S, A, B, C) with `--tier` flag or dynamically with `T` key
|
|
67
|
+
- **๐ Privacy-first analytics (optional)** โ anonymous PostHog events with explicit consent + opt-out
|
|
67
68
|
|
|
68
69
|
---
|
|
69
70
|
|
|
@@ -85,7 +86,7 @@ Before using `free-coding-models`, make sure you have:
|
|
|
85
86
|
3. **OpenCode** *(optional)* โ [Install OpenCode](https://github.com/opencode-ai/opencode) to use the OpenCode integration
|
|
86
87
|
4. **OpenClaw** *(optional)* โ [Install OpenClaw](https://openclaw.ai) to use the OpenClaw integration
|
|
87
88
|
|
|
88
|
-
> ๐ก **Tip:** You don't need all
|
|
89
|
+
> ๐ก **Tip:** You don't need all nine providers. One key is enough to get started. Add more later via the Settings screen (`P` key). Models without a key still show real latency (`๐ NO KEY`) so you can evaluate providers before signing up.
|
|
89
90
|
|
|
90
91
|
---
|
|
91
92
|
|
|
@@ -130,6 +131,9 @@ free-coding-models --best
|
|
|
130
131
|
# Analyze for 10 seconds and output the most reliable model
|
|
131
132
|
free-coding-models --fiable
|
|
132
133
|
|
|
134
|
+
# Disable anonymous analytics for this run
|
|
135
|
+
free-coding-models --no-telemetry
|
|
136
|
+
|
|
133
137
|
# Filter models by tier letter
|
|
134
138
|
free-coding-models --tier S # S+ and S only
|
|
135
139
|
free-coding-models --tier A # A+, A, A- only
|
|
@@ -199,7 +203,7 @@ Setup wizard (first run โ walks through all 9 providers):
|
|
|
199
203
|
You can add or change keys anytime with the P key in the TUI.
|
|
200
204
|
```
|
|
201
205
|
|
|
202
|
-
You don't need all
|
|
206
|
+
You don't need all nine โ skip any provider by pressing Enter. At least one key is required.
|
|
203
207
|
|
|
204
208
|
### Adding or changing keys later
|
|
205
209
|
|
|
@@ -225,6 +229,8 @@ Press **`P`** to open the Settings screen at any time:
|
|
|
225
229
|
|
|
226
230
|
Keys are saved to `~/.free-coding-models.json` (permissions `0600`).
|
|
227
231
|
|
|
232
|
+
Analytics toggle is in the same Settings screen (`P`) as a dedicated row (toggle with Enter or Space).
|
|
233
|
+
|
|
228
234
|
### Environment variable overrides
|
|
229
235
|
|
|
230
236
|
Env vars always take priority over the config file:
|
|
@@ -233,8 +239,19 @@ Env vars always take priority over the config file:
|
|
|
233
239
|
NVIDIA_API_KEY=nvapi-xxx free-coding-models
|
|
234
240
|
GROQ_API_KEY=gsk_xxx free-coding-models
|
|
235
241
|
CEREBRAS_API_KEY=csk_xxx free-coding-models
|
|
242
|
+
FREE_CODING_MODELS_TELEMETRY=0 free-coding-models
|
|
236
243
|
```
|
|
237
244
|
|
|
245
|
+
Telemetry env vars:
|
|
246
|
+
|
|
247
|
+
- `FREE_CODING_MODELS_TELEMETRY=0|1` โ force disable/enable analytics
|
|
248
|
+
- `FREE_CODING_MODELS_POSTHOG_KEY` โ PostHog project API key (required to send events)
|
|
249
|
+
- `FREE_CODING_MODELS_POSTHOG_HOST` โ optional ingest host (`https://eu.i.posthog.com` default)
|
|
250
|
+
- `FREE_CODING_MODELS_TELEMETRY_DEBUG=1` โ optional stderr debug logs for telemetry troubleshooting
|
|
251
|
+
|
|
252
|
+
On first run (or when consent policy changes), the CLI asks users to accept or decline anonymous analytics.
|
|
253
|
+
When enabled, telemetry events include: event name, app version, selected mode, system (`macOS`/`Windows`/`Linux`), and terminal family (`Terminal.app`, `iTerm2`, `kitty`, `Warp`, `WezTerm`, etc., with generic fallback from `TERM_PROGRAM`/`TERM`).
|
|
254
|
+
|
|
238
255
|
### Get your free API keys
|
|
239
256
|
|
|
240
257
|
**NVIDIA NIM** (44 models, S+ โ C tier):
|
|
@@ -500,11 +517,14 @@ This script:
|
|
|
500
517
|
|
|
501
518
|
**Environment variables (override config file):**
|
|
502
519
|
|
|
503
|
-
| Variable |
|
|
504
|
-
|
|
505
|
-
| `NVIDIA_API_KEY` | NVIDIA NIM |
|
|
506
|
-
| `GROQ_API_KEY` | Groq |
|
|
507
|
-
| `CEREBRAS_API_KEY` | Cerebras |
|
|
520
|
+
| Variable | Description |
|
|
521
|
+
|----------|-------------|
|
|
522
|
+
| `NVIDIA_API_KEY` | NVIDIA NIM key |
|
|
523
|
+
| `GROQ_API_KEY` | Groq key |
|
|
524
|
+
| `CEREBRAS_API_KEY` | Cerebras key |
|
|
525
|
+
| `FREE_CODING_MODELS_TELEMETRY` | `0` disables analytics, `1` enables analytics |
|
|
526
|
+
| `FREE_CODING_MODELS_POSTHOG_KEY` | PostHog project API key used for anonymous event capture |
|
|
527
|
+
| `FREE_CODING_MODELS_POSTHOG_HOST` | Optional PostHog ingest host (`https://eu.i.posthog.com` default) |
|
|
508
528
|
|
|
509
529
|
**Config file:** `~/.free-coding-models.json` (created automatically, permissions `0600`)
|
|
510
530
|
|
|
@@ -519,6 +539,11 @@ This script:
|
|
|
519
539
|
"nvidia": { "enabled": true },
|
|
520
540
|
"groq": { "enabled": true },
|
|
521
541
|
"cerebras": { "enabled": true }
|
|
542
|
+
},
|
|
543
|
+
"telemetry": {
|
|
544
|
+
"enabled": true,
|
|
545
|
+
"consentVersion": 1,
|
|
546
|
+
"anonymousId": "anon_550e8400-e29b-41d4-a716-446655440000"
|
|
522
547
|
}
|
|
523
548
|
}
|
|
524
549
|
```
|
|
@@ -538,6 +563,7 @@ This script:
|
|
|
538
563
|
| `--openclaw` | OpenClaw mode โ Enter sets selected model as default in OpenClaw |
|
|
539
564
|
| `--best` | Show only top-tier models (A+, S, S+) |
|
|
540
565
|
| `--fiable` | Analyze 10 seconds, output the most reliable model as `provider/model_id` |
|
|
566
|
+
| `--no-telemetry` | Disable anonymous analytics for this run |
|
|
541
567
|
| `--tier S` | Show only S+ and S tier models |
|
|
542
568
|
| `--tier A` | Show only A+, A, A- tier models |
|
|
543
569
|
| `--tier B` | Show only B+, B tier models |
|
|
@@ -549,15 +575,15 @@ This script:
|
|
|
549
575
|
- **R/Y/O/M/L/A/S/N/H/V/U** โ Sort by Rank/Tier/Origin/Model/LatestPing/Avg/SWE/Ctx/Health/Verdict/Uptime
|
|
550
576
|
- **T** โ Cycle tier filter (All โ S+ โ S โ A+ โ A โ A- โ B+ โ B โ C โ All)
|
|
551
577
|
- **Z** โ Cycle mode (OpenCode CLI โ OpenCode Desktop โ OpenClaw)
|
|
552
|
-
- **P** โ Open Settings (manage API keys,
|
|
578
|
+
- **P** โ Open Settings (manage API keys, provider toggles, analytics toggle)
|
|
553
579
|
- **W** โ Decrease ping interval (faster pings)
|
|
554
580
|
- **X** โ Increase ping interval (slower pings)
|
|
555
581
|
- **Ctrl+C** โ Exit
|
|
556
582
|
|
|
557
583
|
**Keyboard shortcuts (Settings screen โ `P` key):**
|
|
558
|
-
- **โโ** โ Navigate providers
|
|
559
|
-
- **Enter** โ Edit API key inline
|
|
560
|
-
- **Space** โ Toggle provider enabled/disabled
|
|
584
|
+
- **โโ** โ Navigate providers and analytics row
|
|
585
|
+
- **Enter** โ Edit API key inline, or toggle analytics on analytics row
|
|
586
|
+
- **Space** โ Toggle provider enabled/disabled, or toggle analytics on analytics row
|
|
561
587
|
- **T** โ Test current provider's API key (fires a live ping)
|
|
562
588
|
- **Esc** โ Close settings and return to main TUI
|
|
563
589
|
|
|
@@ -615,6 +641,6 @@ We welcome contributions! Feel free to open issues, submit pull requests, or get
|
|
|
615
641
|
|
|
616
642
|
For questions or issues, open a [GitHub issue](https://github.com/vava-nessa/free-coding-models/issues).
|
|
617
643
|
|
|
618
|
-
๐ฌ Let's talk about the
|
|
644
|
+
๐ฌ Let's talk about the project on Discord: https://discord.gg/5MbTnDC3Md
|
|
619
645
|
|
|
620
646
|
> โ ๏ธ **free-coding-models is a BETA TUI** โ it might crash or have problems. Use at your own risk and feel free to report issues!
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
*
|
|
28
28
|
* โ Functions:
|
|
29
29
|
* - `loadConfig` / `saveConfig` / `getApiKey`: Multi-provider JSON config via lib/config.js
|
|
30
|
+
* - `promptTelemetryConsent`: First-run consent flow for anonymous analytics
|
|
31
|
+
* - `getTelemetryDistinctId`: Generate/reuse a stable anonymous ID for telemetry
|
|
32
|
+
* - `getTelemetryTerminal`: Infer terminal family (Terminal.app, iTerm2, kitty, etc.)
|
|
33
|
+
* - `isTelemetryDebugEnabled` / `telemetryDebug`: Optional runtime telemetry diagnostics via env
|
|
34
|
+
* - `sendUsageTelemetry`: Fire-and-forget anonymous app-start event
|
|
30
35
|
* - `promptApiKey`: Interactive wizard for first-time NVIDIA API key setup
|
|
31
36
|
* - `promptModeSelection`: Startup menu to choose OpenCode vs OpenClaw
|
|
32
37
|
* - `ping`: Perform HTTP request to NIM endpoint with timeout handling
|
|
@@ -67,6 +72,7 @@
|
|
|
67
72
|
* - --openclaw: OpenClaw mode (set selected model as default in OpenClaw)
|
|
68
73
|
* - --best: Show only top-tier models (A+, S, S+)
|
|
69
74
|
* - --fiable: Analyze 10s and output the most reliable model
|
|
75
|
+
* - --no-telemetry: Disable anonymous usage analytics for this run
|
|
70
76
|
* - --tier S/A/B/C: Filter models by tier letter (S=S+/S, A=A+/A/A-, B=B+/B, C=C)
|
|
71
77
|
*
|
|
72
78
|
* @see {@link https://build.nvidia.com} NVIDIA API key generation
|
|
@@ -77,6 +83,7 @@
|
|
|
77
83
|
import chalk from 'chalk'
|
|
78
84
|
import { createRequire } from 'module'
|
|
79
85
|
import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from 'fs'
|
|
86
|
+
import { randomUUID } from 'crypto'
|
|
80
87
|
import { homedir } from 'os'
|
|
81
88
|
import { join, dirname } from 'path'
|
|
82
89
|
import { MODELS, sources } from '../sources.js'
|
|
@@ -90,6 +97,322 @@ const readline = require('readline')
|
|
|
90
97
|
// โโโ Version check โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
91
98
|
const pkg = require('../package.json')
|
|
92
99
|
const LOCAL_VERSION = pkg.version
|
|
100
|
+
const TELEMETRY_CONSENT_VERSION = 1
|
|
101
|
+
const TELEMETRY_TIMEOUT = 1_200
|
|
102
|
+
const POSTHOG_CAPTURE_PATH = '/i/v0/e/'
|
|
103
|
+
const POSTHOG_DEFAULT_HOST = 'https://eu.i.posthog.com'
|
|
104
|
+
// ๐ Consent ASCII banner shown before telemetry choice to make first-run intent explicit.
|
|
105
|
+
const TELEMETRY_CONSENT_ASCII = [
|
|
106
|
+
'โโโโโโโ โโโโโโ โโโโโโโ โโโโโโโ โโโโโโ โโโโโโ โโโโโโ โโ โโโ โโ โโโโโโ โโโ โโโ โโโโโโ โโโโโโ โโโโโโโ โโ โโโโโโโ',
|
|
107
|
+
'โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโโโ โโ โโ โโโโ โโโโ โโ โโ โโ โโ โโ โโ โโ',
|
|
108
|
+
'โโโโโ โโโโโโ โโโโโ โโโโโ โโโโโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโโ โโโโโ โโ โโโโ โโ โโ โโ โโ โโ โโโโโ โโ โโโโโโโ',
|
|
109
|
+
'โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ โโ',
|
|
110
|
+
'โโ โโ โโ โโโโโโโ โโโโโโโ โโโโโโ โโโโโโ โโโโโโ โโ โโ โโโโ โโโโโโ โโ โโ โโโโโโ โโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ',
|
|
111
|
+
'',
|
|
112
|
+
'',
|
|
113
|
+
]
|
|
114
|
+
// ๐ Maintainer defaults for global npm telemetry (safe to publish: project key is a public ingest token).
|
|
115
|
+
const POSTHOG_PROJECT_KEY_DEFAULT = 'phc_5P1n8HaLof6nHM0tKJYt4bV5pj2XPb272fLVigwf1YQ'
|
|
116
|
+
const POSTHOG_HOST_DEFAULT = 'https://eu.i.posthog.com'
|
|
117
|
+
|
|
118
|
+
// ๐ parseTelemetryEnv: Convert env var strings into booleans.
|
|
119
|
+
// ๐ Returns true/false when value is recognized, otherwise null.
|
|
120
|
+
function parseTelemetryEnv(value) {
|
|
121
|
+
if (typeof value !== 'string') return null
|
|
122
|
+
const normalized = value.trim().toLowerCase()
|
|
123
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
|
|
124
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ๐ Optional debug switch for telemetry troubleshooting (disabled by default).
|
|
129
|
+
function isTelemetryDebugEnabled() {
|
|
130
|
+
return parseTelemetryEnv(process.env.FREE_CODING_MODELS_TELEMETRY_DEBUG) === true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ๐ Writes telemetry debug traces to stderr only when explicitly enabled.
|
|
134
|
+
function telemetryDebug(message, meta = null) {
|
|
135
|
+
if (!isTelemetryDebugEnabled()) return
|
|
136
|
+
const prefix = '[telemetry-debug]'
|
|
137
|
+
if (meta === null) {
|
|
138
|
+
process.stderr.write(`${prefix} ${message}\n`)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
process.stderr.write(`${prefix} ${message} ${JSON.stringify(meta)}\n`)
|
|
143
|
+
} catch {
|
|
144
|
+
process.stderr.write(`${prefix} ${message}\n`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ๐ Ensure telemetry config shape exists even on old config files.
|
|
149
|
+
function ensureTelemetryConfig(config) {
|
|
150
|
+
if (!config.telemetry || typeof config.telemetry !== 'object') {
|
|
151
|
+
config.telemetry = { enabled: null, consentVersion: 0, anonymousId: null }
|
|
152
|
+
}
|
|
153
|
+
if (typeof config.telemetry.enabled !== 'boolean') config.telemetry.enabled = null
|
|
154
|
+
if (typeof config.telemetry.consentVersion !== 'number') config.telemetry.consentVersion = 0
|
|
155
|
+
if (typeof config.telemetry.anonymousId !== 'string' || !config.telemetry.anonymousId.trim()) {
|
|
156
|
+
config.telemetry.anonymousId = null
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ๐ Create or reuse a persistent anonymous distinct_id for PostHog.
|
|
161
|
+
// ๐ Stored locally in config so one user is stable over time without personal data.
|
|
162
|
+
function getTelemetryDistinctId(config) {
|
|
163
|
+
ensureTelemetryConfig(config)
|
|
164
|
+
if (config.telemetry.anonymousId) return config.telemetry.anonymousId
|
|
165
|
+
|
|
166
|
+
config.telemetry.anonymousId = `anon_${randomUUID()}`
|
|
167
|
+
saveConfig(config)
|
|
168
|
+
return config.telemetry.anonymousId
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ๐ Convert Node platform to human-readable system name for analytics segmentation.
|
|
172
|
+
function getTelemetrySystem() {
|
|
173
|
+
if (process.platform === 'darwin') return 'macOS'
|
|
174
|
+
if (process.platform === 'win32') return 'Windows'
|
|
175
|
+
if (process.platform === 'linux') return 'Linux'
|
|
176
|
+
return process.platform
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ๐ Infer terminal family from environment hints for coarse usage segmentation.
|
|
180
|
+
// ๐ Never sends full env dumps; only a normalized terminal label is emitted.
|
|
181
|
+
function getTelemetryTerminal() {
|
|
182
|
+
const termProgramRaw = (process.env.TERM_PROGRAM || '').trim()
|
|
183
|
+
const termProgram = termProgramRaw.toLowerCase()
|
|
184
|
+
const term = (process.env.TERM || '').toLowerCase()
|
|
185
|
+
|
|
186
|
+
if (termProgram === 'apple_terminal') return 'Terminal.app'
|
|
187
|
+
if (termProgram === 'iterm.app') return 'iTerm2'
|
|
188
|
+
if (termProgram === 'warpterminal' || process.env.WARP_IS_LOCAL_SHELL_SESSION) return 'Warp'
|
|
189
|
+
if (process.env.WT_SESSION) return 'Windows Terminal'
|
|
190
|
+
if (process.env.KITTY_WINDOW_ID || term.includes('kitty')) return 'kitty'
|
|
191
|
+
if (process.env.GHOSTTY_RESOURCES_DIR || term.includes('ghostty')) return 'Ghostty'
|
|
192
|
+
if (process.env.WEZTERM_PANE || termProgram === 'wezterm') return 'WezTerm'
|
|
193
|
+
if (process.env.KONSOLE_VERSION || termProgram === 'konsole') return 'Konsole'
|
|
194
|
+
if (process.env.GNOME_TERMINAL_SCREEN || termProgram === 'gnome-terminal') return 'GNOME Terminal'
|
|
195
|
+
if (process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm') return 'JetBrains Terminal'
|
|
196
|
+
if (process.env.TABBY_CONFIG_DIRECTORY || termProgram === 'tabby') return 'Tabby'
|
|
197
|
+
if (termProgram === 'vscode' || process.env.VSCODE_GIT_IPC_HANDLE) return 'VS Code Terminal'
|
|
198
|
+
if (process.env.ALACRITTY_SOCKET || term.includes('alacritty') || termProgram === 'alacritty') return 'Alacritty'
|
|
199
|
+
if (term.includes('foot') || termProgram === 'foot') return 'foot'
|
|
200
|
+
if (termProgram === 'hyper' || process.env.HYPER) return 'Hyper'
|
|
201
|
+
if (process.env.TMUX) return 'tmux'
|
|
202
|
+
if (process.env.STY) return 'screen'
|
|
203
|
+
// ๐ Generic fallback for many terminals exposing TERM_PROGRAM (e.g., Rio, Contour, etc.).
|
|
204
|
+
if (termProgramRaw) return termProgramRaw
|
|
205
|
+
if (term) return term
|
|
206
|
+
|
|
207
|
+
return 'unknown'
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ๐ Prompt consent on first run (or when consent schema version changes).
|
|
211
|
+
// ๐ This prompt is skipped when the env var explicitly controls telemetry.
|
|
212
|
+
async function promptTelemetryConsent(config, cliArgs) {
|
|
213
|
+
if (cliArgs.noTelemetry) return
|
|
214
|
+
|
|
215
|
+
const envTelemetry = parseTelemetryEnv(process.env.FREE_CODING_MODELS_TELEMETRY)
|
|
216
|
+
if (envTelemetry !== null) return
|
|
217
|
+
|
|
218
|
+
ensureTelemetryConfig(config)
|
|
219
|
+
const hasStoredChoice = typeof config.telemetry.enabled === 'boolean'
|
|
220
|
+
const isConsentCurrent = config.telemetry.consentVersion >= TELEMETRY_CONSENT_VERSION
|
|
221
|
+
if (hasStoredChoice && isConsentCurrent) return
|
|
222
|
+
|
|
223
|
+
// ๐ Non-interactive runs should never hang waiting for input.
|
|
224
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
225
|
+
// ๐ Do not mutate persisted consent in headless runs.
|
|
226
|
+
// ๐ We simply skip the prompt; runtime telemetry remains governed by env/config precedence.
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const options = [
|
|
231
|
+
{ label: 'Accept & Continue', value: true, emoji: '๐๐ฅฐ๐' },
|
|
232
|
+
{ label: 'Reject and Continue', value: false, emoji: '๐ข' },
|
|
233
|
+
]
|
|
234
|
+
let selected = 0 // ๐ Default selection is Accept & Continue.
|
|
235
|
+
|
|
236
|
+
const accepted = await new Promise((resolve) => {
|
|
237
|
+
const render = () => {
|
|
238
|
+
const EL = '\x1b[K'
|
|
239
|
+
const lines = []
|
|
240
|
+
for (const asciiLine of TELEMETRY_CONSENT_ASCII) {
|
|
241
|
+
lines.push(chalk.greenBright(asciiLine))
|
|
242
|
+
}
|
|
243
|
+
lines.push(chalk.greenBright(`free-coding-models (v${LOCAL_VERSION})`))
|
|
244
|
+
lines.push(chalk.greenBright('Welcome ! Would you like to help improve the app and fix bugs by activating PostHog telemetry (anonymous & secure)'))
|
|
245
|
+
lines.push(chalk.greenBright("anonymous telemetry analytics (we don't collect anything from you)"))
|
|
246
|
+
lines.push('')
|
|
247
|
+
|
|
248
|
+
for (let i = 0; i < options.length; i++) {
|
|
249
|
+
const isSelected = i === selected
|
|
250
|
+
const option = options[i]
|
|
251
|
+
const buttonText = `${option.emoji} ${option.label}`
|
|
252
|
+
|
|
253
|
+
let button
|
|
254
|
+
if (isSelected && option.value) button = chalk.black.bgGreenBright(` ${buttonText} `)
|
|
255
|
+
else if (isSelected && !option.value) button = chalk.black.bgRedBright(` ${buttonText} `)
|
|
256
|
+
else if (option.value) button = chalk.greenBright(` ${buttonText} `)
|
|
257
|
+
else button = chalk.redBright(` ${buttonText} `)
|
|
258
|
+
|
|
259
|
+
const prefix = isSelected ? chalk.cyan(' โฏ ') : chalk.dim(' ')
|
|
260
|
+
lines.push(prefix + button)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
lines.push('')
|
|
264
|
+
lines.push(chalk.dim(' โโ Navigate โข Enter Select'))
|
|
265
|
+
lines.push(chalk.dim(' You can change this later in Settings (P).'))
|
|
266
|
+
lines.push('')
|
|
267
|
+
|
|
268
|
+
// ๐ Avoid full-screen clear escape here to prevent title/header offset issues in some terminals.
|
|
269
|
+
const cleared = lines.map(l => l + EL)
|
|
270
|
+
const terminalRows = process.stdout.rows || 24
|
|
271
|
+
const remaining = Math.max(0, terminalRows - cleared.length)
|
|
272
|
+
for (let i = 0; i < remaining; i++) cleared.push(EL)
|
|
273
|
+
process.stdout.write('\x1b[H' + cleared.join('\n'))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const cleanup = () => {
|
|
277
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
278
|
+
process.stdin.removeListener('keypress', onKeyPress)
|
|
279
|
+
process.stdin.pause()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const onKeyPress = (_str, key) => {
|
|
283
|
+
if (!key) return
|
|
284
|
+
|
|
285
|
+
if (key.ctrl && key.name === 'c') {
|
|
286
|
+
cleanup()
|
|
287
|
+
resolve(false)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if ((key.name === 'up' || key.name === 'left') && selected > 0) {
|
|
292
|
+
selected--
|
|
293
|
+
render()
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if ((key.name === 'down' || key.name === 'right') && selected < options.length - 1) {
|
|
298
|
+
selected++
|
|
299
|
+
render()
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (key.name === 'return') {
|
|
304
|
+
cleanup()
|
|
305
|
+
resolve(options[selected].value)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
readline.emitKeypressEvents(process.stdin)
|
|
310
|
+
process.stdin.setEncoding('utf8')
|
|
311
|
+
process.stdin.resume()
|
|
312
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true)
|
|
313
|
+
process.stdin.on('keypress', onKeyPress)
|
|
314
|
+
render()
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
config.telemetry.enabled = accepted
|
|
318
|
+
config.telemetry.consentVersion = TELEMETRY_CONSENT_VERSION
|
|
319
|
+
saveConfig(config)
|
|
320
|
+
|
|
321
|
+
console.log()
|
|
322
|
+
if (accepted) {
|
|
323
|
+
console.log(chalk.green(' โ
Analytics enabled. You can disable it later in Settings (P) or with --no-telemetry.'))
|
|
324
|
+
} else {
|
|
325
|
+
console.log(chalk.yellow(' Analytics disabled. You can enable it later in Settings (P).'))
|
|
326
|
+
}
|
|
327
|
+
console.log()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ๐ Resolve telemetry effective state with clear precedence:
|
|
331
|
+
// ๐ CLI flag > env var > config file > disabled by default.
|
|
332
|
+
function isTelemetryEnabled(config, cliArgs) {
|
|
333
|
+
if (cliArgs.noTelemetry) return false
|
|
334
|
+
const envTelemetry = parseTelemetryEnv(process.env.FREE_CODING_MODELS_TELEMETRY)
|
|
335
|
+
if (envTelemetry !== null) return envTelemetry
|
|
336
|
+
ensureTelemetryConfig(config)
|
|
337
|
+
return config.telemetry.enabled === true
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ๐ Fire-and-forget analytics ping: never blocks UX, never throws.
|
|
341
|
+
async function sendUsageTelemetry(config, cliArgs, payload) {
|
|
342
|
+
if (!isTelemetryEnabled(config, cliArgs)) {
|
|
343
|
+
telemetryDebug('skip: telemetry disabled', {
|
|
344
|
+
cliNoTelemetry: cliArgs.noTelemetry === true,
|
|
345
|
+
envTelemetry: process.env.FREE_CODING_MODELS_TELEMETRY || null,
|
|
346
|
+
configEnabled: config?.telemetry?.enabled ?? null,
|
|
347
|
+
})
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const apiKey = (
|
|
352
|
+
process.env.FREE_CODING_MODELS_POSTHOG_KEY ||
|
|
353
|
+
process.env.POSTHOG_PROJECT_API_KEY ||
|
|
354
|
+
POSTHOG_PROJECT_KEY_DEFAULT ||
|
|
355
|
+
''
|
|
356
|
+
).trim()
|
|
357
|
+
if (!apiKey) {
|
|
358
|
+
telemetryDebug('skip: missing api key')
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const host = (
|
|
363
|
+
process.env.FREE_CODING_MODELS_POSTHOG_HOST ||
|
|
364
|
+
process.env.POSTHOG_HOST ||
|
|
365
|
+
POSTHOG_HOST_DEFAULT ||
|
|
366
|
+
POSTHOG_DEFAULT_HOST
|
|
367
|
+
).trim().replace(/\/+$/, '')
|
|
368
|
+
if (!host) {
|
|
369
|
+
telemetryDebug('skip: missing host')
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const endpoint = `${host}${POSTHOG_CAPTURE_PATH}`
|
|
375
|
+
const distinctId = getTelemetryDistinctId(config)
|
|
376
|
+
const timestamp = typeof payload?.ts === 'string' ? payload.ts : new Date().toISOString()
|
|
377
|
+
const signal = (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function')
|
|
378
|
+
? AbortSignal.timeout(TELEMETRY_TIMEOUT)
|
|
379
|
+
: undefined
|
|
380
|
+
|
|
381
|
+
const posthogBody = {
|
|
382
|
+
api_key: apiKey,
|
|
383
|
+
event: payload?.event || 'app_start',
|
|
384
|
+
distinct_id: distinctId,
|
|
385
|
+
timestamp,
|
|
386
|
+
properties: {
|
|
387
|
+
$process_person_profile: false,
|
|
388
|
+
source: 'cli',
|
|
389
|
+
app: 'free-coding-models',
|
|
390
|
+
version: payload?.version || LOCAL_VERSION,
|
|
391
|
+
app_version: payload?.version || LOCAL_VERSION,
|
|
392
|
+
mode: payload?.mode || 'opencode',
|
|
393
|
+
system: getTelemetrySystem(),
|
|
394
|
+
terminal: getTelemetryTerminal(),
|
|
395
|
+
},
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
await fetch(endpoint, {
|
|
399
|
+
method: 'POST',
|
|
400
|
+
headers: { 'content-type': 'application/json' },
|
|
401
|
+
body: JSON.stringify(posthogBody),
|
|
402
|
+
signal,
|
|
403
|
+
})
|
|
404
|
+
telemetryDebug('sent', {
|
|
405
|
+
event: posthogBody.event,
|
|
406
|
+
endpoint,
|
|
407
|
+
mode: posthogBody.properties.mode,
|
|
408
|
+
system: posthogBody.properties.system,
|
|
409
|
+
terminal: posthogBody.properties.terminal,
|
|
410
|
+
})
|
|
411
|
+
} catch {
|
|
412
|
+
// ๐ Ignore failures silently: analytics must never break the CLI.
|
|
413
|
+
telemetryDebug('error: send failed')
|
|
414
|
+
}
|
|
415
|
+
}
|
|
93
416
|
|
|
94
417
|
async function checkForUpdate() {
|
|
95
418
|
try {
|
|
@@ -440,14 +763,17 @@ const spinCell = (f, o = 0) => chalk.dim.yellow(FRAMES[(f + o) % FRAMES.length].
|
|
|
440
763
|
// ๐ are imported from lib/utils.js for testability
|
|
441
764
|
|
|
442
765
|
// โโโ Viewport calculation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
766
|
+
// ๐ Keep these constants in sync with renderTable() fixed shell lines.
|
|
767
|
+
// ๐ If this drifts, model rows overflow and can push the title row out of view.
|
|
768
|
+
const TABLE_HEADER_LINES = 4 // ๐ title, spacer, column headers, separator
|
|
769
|
+
const TABLE_FOOTER_LINES = 6 // ๐ spacer, hints, spacer, credit+contributors, discord, spacer
|
|
770
|
+
const TABLE_FIXED_LINES = TABLE_HEADER_LINES + TABLE_FOOTER_LINES
|
|
771
|
+
|
|
443
772
|
// ๐ Computes the visible slice of model rows that fits in the terminal.
|
|
444
|
-
// ๐ Fixed lines: 5 header + 5 footer = 10 lines always consumed.
|
|
445
|
-
// ๐ Header: empty, title, empty, column headers, separator (5)
|
|
446
|
-
// ๐ Footer: empty, hints, empty, credit, empty (5)
|
|
447
773
|
// ๐ When scroll indicators are needed, they each consume 1 line from the model budget.
|
|
448
774
|
function calculateViewport(terminalRows, scrollOffset, totalModels) {
|
|
449
775
|
if (terminalRows <= 0) return { startIdx: 0, endIdx: totalModels, hasAbove: false, hasBelow: false }
|
|
450
|
-
let maxSlots = terminalRows -
|
|
776
|
+
let maxSlots = terminalRows - TABLE_FIXED_LINES
|
|
451
777
|
if (maxSlots < 1) maxSlots = 1
|
|
452
778
|
if (totalModels <= maxSlots) return { startIdx: 0, endIdx: totalModels, hasAbove: false, hasBelow: false }
|
|
453
779
|
|
|
@@ -486,7 +812,7 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
|
|
|
486
812
|
if (mode === 'openclaw') {
|
|
487
813
|
modeBadge = chalk.bold.rgb(255, 100, 50)(' [๐ฆ OpenClaw]')
|
|
488
814
|
} else if (mode === 'opencode-desktop') {
|
|
489
|
-
modeBadge = chalk.bold.rgb(0, 200, 255)(' [๐ฅ
|
|
815
|
+
modeBadge = chalk.bold.rgb(0, 200, 255)(' [๐ฅ Desktop]')
|
|
490
816
|
} else {
|
|
491
817
|
modeBadge = chalk.bold.rgb(0, 200, 255)(' [๐ป CLI]')
|
|
492
818
|
}
|
|
@@ -529,7 +855,6 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
|
|
|
529
855
|
const sorted = sortResults(visibleResults, sortColumn, sortDirection)
|
|
530
856
|
|
|
531
857
|
const lines = [
|
|
532
|
-
'',
|
|
533
858
|
` ${chalk.bold('โก Free Coding Models')} ${chalk.dim('v' + LOCAL_VERSION)}${modeBadge}${modeHint}${tierBadge}${originBadge} ` +
|
|
534
859
|
chalk.greenBright(`โ
${up}`) + chalk.dim(' up ') +
|
|
535
860
|
chalk.yellow(`โณ ${timeout}`) + chalk.dim(' timeout ') +
|
|
@@ -771,7 +1096,15 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
|
|
|
771
1096
|
: chalk.rgb(0, 200, 255)('EnterโOpenCode')
|
|
772
1097
|
lines.push(chalk.dim(` โโ Navigate โข `) + actionHint + chalk.dim(` โข R/Y/O/M/L/A/S/C/H/V/U Sort โข T Tier โข N Origin โข Wโ/Xโ (${intervalSec}s) โข Z Mode โข `) + chalk.yellow('P') + chalk.dim(` Settings โข `) + chalk.bgGreenBright.black.bold(' K Help ') + chalk.dim(` โข Ctrl+C Exit`))
|
|
773
1098
|
lines.push('')
|
|
774
|
-
lines.push(
|
|
1099
|
+
lines.push(
|
|
1100
|
+
chalk.rgb(255, 150, 200)(' Made with ๐ & โ by \x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\') +
|
|
1101
|
+
chalk.dim(' โข ') +
|
|
1102
|
+
'โญ ' +
|
|
1103
|
+
'\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\Star on GitHub\x1b]8;;\x1b\\' +
|
|
1104
|
+
chalk.dim(' โข ') +
|
|
1105
|
+
'๐ค ' +
|
|
1106
|
+
'\x1b]8;;https://github.com/vava-nessa/free-coding-models/graphs/contributors\x1b\\Contributors\x1b]8;;\x1b\\'
|
|
1107
|
+
)
|
|
775
1108
|
// ๐ Discord invite + BETA warning โ always visible at the bottom of the TUI
|
|
776
1109
|
lines.push(' ๐ฌ ' + chalk.cyanBright('\x1b]8;;https://discord.gg/5MbTnDC3Md\x1b\\Join our Discord\x1b]8;;\x1b\\') + chalk.dim(' โ ') + chalk.cyanBright('https://discord.gg/5MbTnDC3Md') + chalk.dim(' โข ') + chalk.yellow('โ BETA TUI') + chalk.dim(' โ might crash or have problems'))
|
|
777
1110
|
lines.push('')
|
|
@@ -1585,6 +1918,7 @@ async function main() {
|
|
|
1585
1918
|
|
|
1586
1919
|
// ๐ Load JSON config (auto-migrates old plain-text ~/.free-coding-models if needed)
|
|
1587
1920
|
const config = loadConfig()
|
|
1921
|
+
ensureTelemetryConfig(config)
|
|
1588
1922
|
|
|
1589
1923
|
// ๐ Check if any provider has a key โ if not, run the first-time setup wizard
|
|
1590
1924
|
const hasAnyKey = Object.keys(sources).some(pk => !!getApiKey(config, pk))
|
|
@@ -1600,9 +1934,27 @@ async function main() {
|
|
|
1600
1934
|
}
|
|
1601
1935
|
}
|
|
1602
1936
|
|
|
1937
|
+
// ๐ Ask analytics consent only when not explicitly controlled by env or CLI flag.
|
|
1938
|
+
await promptTelemetryConsent(config, cliArgs)
|
|
1939
|
+
|
|
1603
1940
|
// ๐ Backward-compat: keep apiKey var for startOpenClaw() which still needs it
|
|
1604
1941
|
let apiKey = getApiKey(config, 'nvidia')
|
|
1605
1942
|
|
|
1943
|
+
// ๐ Default mode: OpenCode CLI
|
|
1944
|
+
let mode = 'opencode'
|
|
1945
|
+
if (cliArgs.openClawMode) mode = 'openclaw'
|
|
1946
|
+
else if (cliArgs.openCodeDesktopMode) mode = 'opencode-desktop'
|
|
1947
|
+
else if (cliArgs.openCodeMode) mode = 'opencode'
|
|
1948
|
+
|
|
1949
|
+
// ๐ Track app opening early so fast exits are still counted.
|
|
1950
|
+
// ๐ Must run before update checks because npm registry lookups can add startup delay.
|
|
1951
|
+
void sendUsageTelemetry(config, cliArgs, {
|
|
1952
|
+
event: 'app_start',
|
|
1953
|
+
version: LOCAL_VERSION,
|
|
1954
|
+
mode,
|
|
1955
|
+
ts: new Date().toISOString(),
|
|
1956
|
+
})
|
|
1957
|
+
|
|
1606
1958
|
// ๐ Check for updates in the background
|
|
1607
1959
|
let latestVersion = null
|
|
1608
1960
|
try {
|
|
@@ -1611,9 +1963,6 @@ async function main() {
|
|
|
1611
1963
|
// Silently fail - don't block the app if npm registry is unreachable
|
|
1612
1964
|
}
|
|
1613
1965
|
|
|
1614
|
-
// ๐ Default mode: OpenCode CLI
|
|
1615
|
-
let mode = 'opencode'
|
|
1616
|
-
|
|
1617
1966
|
// ๐ Show update notification menu if a new version is available
|
|
1618
1967
|
if (latestVersion) {
|
|
1619
1968
|
const action = await promptUpdateNotification(latestVersion)
|
|
@@ -1643,6 +1992,7 @@ async function main() {
|
|
|
1643
1992
|
|
|
1644
1993
|
// ๐ Build results from MODELS โ only include enabled providers
|
|
1645
1994
|
// ๐ Each result gets providerKey so ping() knows which URL + API key to use
|
|
1995
|
+
|
|
1646
1996
|
let results = MODELS
|
|
1647
1997
|
.filter(([,,,,,providerKey]) => isProviderEnabled(config, providerKey))
|
|
1648
1998
|
.map(([modelId, label, tier, sweScore, ctx, providerKey], i) => ({
|
|
@@ -1657,7 +2007,7 @@ async function main() {
|
|
|
1657
2007
|
// ๐ Called after every cursor move, sort change, and terminal resize.
|
|
1658
2008
|
const adjustScrollOffset = (st) => {
|
|
1659
2009
|
const total = st.visibleSorted ? st.visibleSorted.length : st.results.filter(r => !r.hidden).length
|
|
1660
|
-
let maxSlots = st.terminalRows -
|
|
2010
|
+
let maxSlots = st.terminalRows - TABLE_FIXED_LINES
|
|
1661
2011
|
if (maxSlots < 1) maxSlots = 1
|
|
1662
2012
|
if (total <= maxSlots) { st.scrollOffset = 0; return }
|
|
1663
2013
|
// Ensure cursor is not above the visible window
|
|
@@ -1754,6 +2104,7 @@ async function main() {
|
|
|
1754
2104
|
// ๐ Key "T" in settings = test API key for selected provider.
|
|
1755
2105
|
function renderSettings() {
|
|
1756
2106
|
const providerKeys = Object.keys(sources)
|
|
2107
|
+
const telemetryRowIdx = providerKeys.length
|
|
1757
2108
|
const EL = '\x1b[K'
|
|
1758
2109
|
const lines = []
|
|
1759
2110
|
|
|
@@ -1798,11 +2149,26 @@ async function main() {
|
|
|
1798
2149
|
lines.push(isCursor ? chalk.bgRgb(30, 30, 60)(row) : row)
|
|
1799
2150
|
}
|
|
1800
2151
|
|
|
2152
|
+
lines.push('')
|
|
2153
|
+
lines.push(` ${chalk.bold('Analytics')}`)
|
|
2154
|
+
lines.push('')
|
|
2155
|
+
|
|
2156
|
+
const telemetryCursor = state.settingsCursor === telemetryRowIdx
|
|
2157
|
+
const telemetryEnabled = state.config.telemetry?.enabled === true
|
|
2158
|
+
const telemetryStatus = telemetryEnabled ? chalk.greenBright('โ
Enabled') : chalk.dim('โฌ Disabled')
|
|
2159
|
+
const telemetryRowBullet = telemetryCursor ? chalk.bold.cyan(' โฏ ') : chalk.dim(' ')
|
|
2160
|
+
const telemetryEnv = parseTelemetryEnv(process.env.FREE_CODING_MODELS_TELEMETRY)
|
|
2161
|
+
const telemetrySource = telemetryEnv === null
|
|
2162
|
+
? chalk.dim('[Config]')
|
|
2163
|
+
: chalk.yellow('[Env override]')
|
|
2164
|
+
const telemetryRow = `${telemetryRowBullet}${chalk.bold('Anonymous usage analytics').padEnd(44)} ${telemetryStatus} ${telemetrySource}`
|
|
2165
|
+
lines.push(telemetryCursor ? chalk.bgRgb(30, 30, 60)(telemetryRow) : telemetryRow)
|
|
2166
|
+
|
|
1801
2167
|
lines.push('')
|
|
1802
2168
|
if (state.settingsEditMode) {
|
|
1803
2169
|
lines.push(chalk.dim(' Type API key โข Enter Save โข Esc Cancel'))
|
|
1804
2170
|
} else {
|
|
1805
|
-
lines.push(chalk.dim(' โโ Navigate โข Enter Edit key โข Space Toggle enabled โข T Test key โข Esc Close'))
|
|
2171
|
+
lines.push(chalk.dim(' โโ Navigate โข Enter Edit key / Toggle analytics โข Space Toggle enabled โข T Test key โข Esc Close'))
|
|
1806
2172
|
}
|
|
1807
2173
|
lines.push('')
|
|
1808
2174
|
|
|
@@ -1838,7 +2204,7 @@ async function main() {
|
|
|
1838
2204
|
lines.push(` ${chalk.yellow('W')} Decrease ping interval (faster)`)
|
|
1839
2205
|
lines.push(` ${chalk.yellow('X')} Increase ping interval (slower)`)
|
|
1840
2206
|
lines.push(` ${chalk.yellow('Z')} Cycle launch mode ${chalk.dim('(OpenCode CLI โ OpenCode Desktop โ OpenClaw)')}`)
|
|
1841
|
-
lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys
|
|
2207
|
+
lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, analytics toggle)')}`)
|
|
1842
2208
|
lines.push(` ${chalk.yellow('K')} / ${chalk.yellow('Esc')} Show/hide this help`)
|
|
1843
2209
|
lines.push(` ${chalk.yellow('Ctrl+C')} Exit`)
|
|
1844
2210
|
lines.push('')
|
|
@@ -1892,6 +2258,7 @@ async function main() {
|
|
|
1892
2258
|
// โโโ Settings overlay keyboard handling โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1893
2259
|
if (state.settingsOpen) {
|
|
1894
2260
|
const providerKeys = Object.keys(sources)
|
|
2261
|
+
const telemetryRowIdx = providerKeys.length
|
|
1895
2262
|
|
|
1896
2263
|
// ๐ Edit mode: capture typed characters for the API key
|
|
1897
2264
|
if (state.settingsEditMode) {
|
|
@@ -1953,12 +2320,20 @@ async function main() {
|
|
|
1953
2320
|
return
|
|
1954
2321
|
}
|
|
1955
2322
|
|
|
1956
|
-
if (key.name === 'down' && state.settingsCursor <
|
|
2323
|
+
if (key.name === 'down' && state.settingsCursor < telemetryRowIdx) {
|
|
1957
2324
|
state.settingsCursor++
|
|
1958
2325
|
return
|
|
1959
2326
|
}
|
|
1960
2327
|
|
|
1961
2328
|
if (key.name === 'return') {
|
|
2329
|
+
if (state.settingsCursor === telemetryRowIdx) {
|
|
2330
|
+
ensureTelemetryConfig(state.config)
|
|
2331
|
+
state.config.telemetry.enabled = state.config.telemetry.enabled !== true
|
|
2332
|
+
state.config.telemetry.consentVersion = TELEMETRY_CONSENT_VERSION
|
|
2333
|
+
saveConfig(state.config)
|
|
2334
|
+
return
|
|
2335
|
+
}
|
|
2336
|
+
|
|
1962
2337
|
// ๐ Enter edit mode for the selected provider's key
|
|
1963
2338
|
const pk = providerKeys[state.settingsCursor]
|
|
1964
2339
|
state.settingsEditBuffer = state.config.apiKeys?.[pk] ?? ''
|
|
@@ -1967,6 +2342,14 @@ async function main() {
|
|
|
1967
2342
|
}
|
|
1968
2343
|
|
|
1969
2344
|
if (key.name === 'space') {
|
|
2345
|
+
if (state.settingsCursor === telemetryRowIdx) {
|
|
2346
|
+
ensureTelemetryConfig(state.config)
|
|
2347
|
+
state.config.telemetry.enabled = state.config.telemetry.enabled !== true
|
|
2348
|
+
state.config.telemetry.consentVersion = TELEMETRY_CONSENT_VERSION
|
|
2349
|
+
saveConfig(state.config)
|
|
2350
|
+
return
|
|
2351
|
+
}
|
|
2352
|
+
|
|
1970
2353
|
// ๐ Toggle enabled/disabled for selected provider
|
|
1971
2354
|
const pk = providerKeys[state.settingsCursor]
|
|
1972
2355
|
if (!state.config.providers) state.config.providers = {}
|
|
@@ -1977,6 +2360,8 @@ async function main() {
|
|
|
1977
2360
|
}
|
|
1978
2361
|
|
|
1979
2362
|
if (key.name === 't') {
|
|
2363
|
+
if (state.settingsCursor === telemetryRowIdx) return
|
|
2364
|
+
|
|
1980
2365
|
// ๐ Test the selected provider's key (fires a real ping)
|
|
1981
2366
|
const pk = providerKeys[state.settingsCursor]
|
|
1982
2367
|
testProviderKey(pk)
|
package/lib/config.js
CHANGED
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
* "hyperbolic": { "enabled": true },
|
|
33
33
|
* "scaleway": { "enabled": true },
|
|
34
34
|
* "googleai": { "enabled": true }
|
|
35
|
+
* },
|
|
36
|
+
* "telemetry": {
|
|
37
|
+
* "enabled": true,
|
|
38
|
+
* "consentVersion": 1,
|
|
39
|
+
* "anonymousId": "anon_550e8400-e29b-41d4-a716-446655440000"
|
|
35
40
|
* }
|
|
36
41
|
* }
|
|
37
42
|
*
|
|
@@ -86,7 +91,7 @@ const ENV_VARS = {
|
|
|
86
91
|
* ๐ The migration reads the old file as a plain nvidia API key and writes
|
|
87
92
|
* a proper JSON config. The old file is NOT deleted (safety first).
|
|
88
93
|
*
|
|
89
|
-
* @returns {{ apiKeys: Record<string,string>, providers: Record<string,{enabled:boolean}
|
|
94
|
+
* @returns {{ apiKeys: Record<string,string>, providers: Record<string,{enabled:boolean}>, telemetry: { enabled: boolean | null, consentVersion: number, anonymousId: string | null } }}
|
|
90
95
|
*/
|
|
91
96
|
export function loadConfig() {
|
|
92
97
|
// ๐ Try new JSON config first
|
|
@@ -97,6 +102,10 @@ export function loadConfig() {
|
|
|
97
102
|
// ๐ Ensure the shape is always complete โ fill missing sections with defaults
|
|
98
103
|
if (!parsed.apiKeys) parsed.apiKeys = {}
|
|
99
104
|
if (!parsed.providers) parsed.providers = {}
|
|
105
|
+
if (!parsed.telemetry || typeof parsed.telemetry !== 'object') parsed.telemetry = { enabled: null, consentVersion: 0, anonymousId: null }
|
|
106
|
+
if (typeof parsed.telemetry.enabled !== 'boolean') parsed.telemetry.enabled = null
|
|
107
|
+
if (typeof parsed.telemetry.consentVersion !== 'number') parsed.telemetry.consentVersion = 0
|
|
108
|
+
if (typeof parsed.telemetry.anonymousId !== 'string' || !parsed.telemetry.anonymousId.trim()) parsed.telemetry.anonymousId = null
|
|
100
109
|
return parsed
|
|
101
110
|
} catch {
|
|
102
111
|
// ๐ Corrupted JSON โ return empty config (user will re-enter keys)
|
|
@@ -129,7 +138,7 @@ export function loadConfig() {
|
|
|
129
138
|
* ๐ Uses mode 0o600 so the file is only readable by the owning user (API keys!).
|
|
130
139
|
* ๐ Pretty-prints JSON for human readability.
|
|
131
140
|
*
|
|
132
|
-
* @param {{ apiKeys: Record<string,string>, providers: Record<string,{enabled:boolean}
|
|
141
|
+
* @param {{ apiKeys: Record<string,string>, providers: Record<string,{enabled:boolean}>, telemetry?: { enabled?: boolean | null, consentVersion?: number, anonymousId?: string | null } }} config
|
|
133
142
|
*/
|
|
134
143
|
export function saveConfig(config) {
|
|
135
144
|
try {
|
|
@@ -184,5 +193,11 @@ function _emptyConfig() {
|
|
|
184
193
|
return {
|
|
185
194
|
apiKeys: {},
|
|
186
195
|
providers: {},
|
|
196
|
+
// ๐ Telemetry consent is explicit. null = not decided yet.
|
|
197
|
+
telemetry: {
|
|
198
|
+
enabled: null,
|
|
199
|
+
consentVersion: 0,
|
|
200
|
+
anonymousId: null,
|
|
201
|
+
},
|
|
187
202
|
}
|
|
188
203
|
}
|
package/lib/utils.js
CHANGED
|
@@ -277,11 +277,11 @@ export function findBestModel(results) {
|
|
|
277
277
|
//
|
|
278
278
|
// ๐ Argument types:
|
|
279
279
|
// - API key: first positional arg that doesn't start with "--" (e.g., "nvapi-xxx")
|
|
280
|
-
// - Boolean flags: --best, --fiable, --opencode, --opencode-desktop, --openclaw (case-insensitive)
|
|
280
|
+
// - Boolean flags: --best, --fiable, --opencode, --opencode-desktop, --openclaw, --no-telemetry (case-insensitive)
|
|
281
281
|
// - Value flag: --tier <letter> (the next non-flag arg is the tier value)
|
|
282
282
|
//
|
|
283
283
|
// ๐ Returns:
|
|
284
|
-
// { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openClawMode, tierFilter }
|
|
284
|
+
// { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openClawMode, noTelemetry, tierFilter }
|
|
285
285
|
//
|
|
286
286
|
// ๐ Note: apiKey may be null here โ the main CLI falls back to env vars and saved config.
|
|
287
287
|
export function parseArgs(argv) {
|
|
@@ -310,8 +310,9 @@ export function parseArgs(argv) {
|
|
|
310
310
|
const openCodeMode = flags.includes('--opencode')
|
|
311
311
|
const openCodeDesktopMode = flags.includes('--opencode-desktop')
|
|
312
312
|
const openClawMode = flags.includes('--openclaw')
|
|
313
|
+
const noTelemetry = flags.includes('--no-telemetry')
|
|
313
314
|
|
|
314
315
|
let tierFilter = tierValueIdx !== -1 ? args[tierValueIdx].toUpperCase() : null
|
|
315
316
|
|
|
316
|
-
return { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openClawMode, tierFilter }
|
|
317
|
+
return { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openClawMode, noTelemetry, tierFilter }
|
|
317
318
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
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",
|