free-coding-models 0.3.42 → 0.3.44

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 CHANGED
@@ -1,12 +1,10 @@
1
- # Changelog
2
- ---
3
-
4
- ## [0.3.42] - 2026-04-09
1
+ ## [0.3.44] - 2026-04-10
5
2
 
6
3
  ### Added
7
- - Added a Star History section to the README so users can quickly see project growth and adoption over time.
4
+ - Added **jcode** external tool support, integrating a new CLI tool across the codebase with command-palette description, tool metadata (label, emoji, flag, color), bootstrap metadata (binary, docs URL, install commands), and full launch wiring via `prepareExternalToolLaunch` and `startExternalTool`. CLI flag parsing for `--jcode` (jcodeMode) is also included.
5
+
6
+ ### Changed
7
+ - Provider API keys are now synced into tool launches — `openclaw` imports `getApiKey` and `syncShellEnv`, accepts an env override in `spawnOpenClawCli`, and populates the child process env with the provider API key when launching the CLI. Tool launchers now use `model.providerKey` instead of hardcoded provider IDs, and `prepareExternalToolLaunch` includes provider and API key in launch args. This enables multi-provider support and ensures launched tools can authenticate using the configured key.
8
8
 
9
9
  ### Fixed
10
- - Xcode Intelligence bootstrap detection now recognizes `xcodebuild`, so the launcher can detect a valid local Xcode install instead of treating the tool as missing.
11
- - `free-coding-models --web` now checks whether port `3333` already belongs to another app and automatically starts on the next free local port instead of sending users to a misleading `Not Found` page.
12
- - npm releases now build the web dashboard during `prepack`, so published installs include `web/dist` and the web UI actually loads after install instead of starting a server with no bundled frontend.
10
+ - README now includes a "Bonus Free Stuff" section with curated resources: community awesome-lists, AI-powered IDEs with free tiers, API providers with permanent free tiers, trial credit providers, and education/developer program freebies — accessible via a new navigation link.
package/README.md CHANGED
@@ -34,11 +34,13 @@ create a free account on one of the [providers](#-list-of-free-ai-providers)
34
34
  <a href="#-why-this-tool">💡 Why</a> •
35
35
  <a href="#-quick-start">⚡ Quick Start</a> •
36
36
  <a href="#-list-of-free-ai-providers">🟢 Providers</a> •
37
+ <a href="#-bonus-free-stuff">🎁 Bonus Free Stuff</a> •
37
38
  <a href="#-usage">🚀 Usage</a> •
38
39
  <a href="#-tui-keys">⌨️ TUI Keys</a> •
39
40
  <a href="#-features">✨ Features</a> •
40
41
  <a href="#-contributing">📋 Contributing</a> •
41
42
  <a href="#️-model-licensing--commercial-use">⚖️ Licensing</a> •
43
+ <a href="#-telemetry">📊 Telemetry</a> •
42
44
  <a href="#️-security--trust">🛡️ Security</a> •
43
45
  <a href="#-support">📧 Support</a> •
44
46
  <a href="#-license">📄 License</a>
@@ -102,6 +104,84 @@ Create a free account on one provider below to get started:
102
104
 
103
105
  > 💡 One key is enough. Add more at any time with **`P`** inside the TUI.
104
106
 
107
+ ---
108
+
109
+ ## 🎁 Bonus Free Stuff
110
+
111
+ **Everything free that isn't in the CLI** — IDE extensions, coding agents, GitHub lists, trial credits, and more.
112
+
113
+ ### 📚 Awesome Lists (curated by the community)
114
+
115
+ | Resource | What it is |
116
+ |----------|------------|
117
+ | [cheahjs/free-llm-api-resources](https://github.com/cheahjs/free-llm-api-resources) (18.4k ⭐) | Comprehensive list of free LLM API providers with rate limits |
118
+ | [mnfst/awesome-free-llm-apis](https://github.com/mnfst/awesome-free-llm-apis) (2.1k ⭐) | Permanent free LLM API tiers organized by provider |
119
+ | [inmve/free-ai-coding](https://github.com/inmve/free-ai-coding) (648 ⭐) | Pro-grade AI coding tools side-by-side — limits, models, CC requirements |
120
+ | [amardeeplakshkar/awesome-free-llm-apis](https://github.com/amardeeplakshkar/awesome-free-llm-apis) | Additional free LLM API resources |
121
+
122
+ ### 🖥️ AI-Powered IDEs with Free Tiers
123
+
124
+ | IDE | Free tier | Credit card |
125
+ |-----|-----------|-------------|
126
+ | [Qwen Code](https://github.com/QwenLM/qwen-code) | 2,000 requests/day | No |
127
+ | [Rovo Dev CLI](https://www.atlassian.com/rovo) | 5M tokens/day (beta) | No |
128
+ | [Gemini CLI](https://github.com/google-gemini/gemini-cli) | 100–250 requests/day | No |
129
+ | [Jules](https://jules.google/) | 15 tasks/day | No |
130
+ | [AWS Kiro](https://kiro.dev/) | 50 credits/month | No |
131
+ | [Trae](https://trae.ai/) | 10 fast + 50 slow requests/month | No |
132
+ | [Codeium](https://codeium.com/) | Unlimited forever, basic models | No |
133
+ | [JetBrains AI Assistant](https://www.jetbrains.com/ai/) | Unlimited completions + local models | No |
134
+ | [Continue.dev](https://www.continue.dev/) | Free VS Code/JetBrains extension, local models via Ollama | No |
135
+ | [Warp](https://warp.dev/) | 150 credits/month (first 2 months), then 75/month | No |
136
+ | [Amazon Q Developer](https://aws.amazon.com/q/developer/) | 50 agentic requests/month | Required |
137
+ | [Windsurf](https://windsurf.com/) | 25 prompt credits/month | Required |
138
+ | [Kilo Code](https://kilocode.ai/) | Up to $25 signup credits (one-time) | Required |
139
+ | [Tabnine](https://www.tabnine.com/) | Basic completions + chat (limited) | Required |
140
+ | [SuperMaven](https://supermaven.com/) | Basic suggestions, 1M token context | Required |
141
+
142
+ ### 🔑 API Providers with Permanent Free Tiers
143
+
144
+ | Provider | Free limits | Notable models |
145
+ |----------|-------------|----------------|
146
+ | [OpenRouter](https://openrouter.ai/keys) | 50 req/day, 1K/day with $10 purchase | Qwen3-Coder, Llama 3.3 70B, Gemma 3 |
147
+ | [Google AI Studio](https://aistudio.google.com/apikey) | 5–500 req/day (varies by model) | Gemini 2.5 Flash, Gemma 3 |
148
+ | [NVIDIA NIM](https://build.nvidia.com) | 40 RPM | Llama 3.3 70B, Mistral Large, Qwen3 235B |
149
+ | [Groq](https://console.groq.com/keys) | 1K–14.4K req/day (model-dependent) | Llama 3.3 70B, Llama 4 Scout, Kimi K2 |
150
+ | [Cerebras](https://cloud.cerebras.ai/) | 30 RPM, 1M tokens/day | Qwen3-235B, Llama 3.1 70B, GPT-OSS 120B |
151
+ | [Cohere](https://cohere.com/) | 20 RPM, 1K/month | Command R+, Aya Expanse 32B |
152
+ | [Mistral La Plateforme](https://console.mistral.ai/) | 1 req/s, 1B tokens/month | Mistral Large 3, Small 3.1 |
153
+ | [Cloudflare Workers AI](https://dash.cloudflare.com) | 10K neurons/day | Llama 3.3 70B, QwQ 32B, 47+ models |
154
+ | [GitHub Models](https://github.com/marketplace/models) | Depends on Copilot tier | GPT-4o, DeepSeek-R1, Llama 3.3 |
155
+ | [SiliconFlow](https://cloud.siliconflow.cn/account/ak) | 1K RPM, 50K TPM | Qwen3-8B, DeepSeek-R1, GLM-4.1V |
156
+ | [HuggingFace](https://huggingface.com/settings/tokens) | ~$0.10/month credits | Llama 3.3 70B, Qwen2.5 72B |
157
+
158
+ ### 💰 Providers with Trial Credits
159
+
160
+ | Provider | Credits | Duration |
161
+ |----------|---------|----------|
162
+ | [Hyperbolic](https://app.hyperbolic.ai/) | $1 free | Permanent |
163
+ | [Fireworks](https://fireworks.ai/) | $1 | Permanent |
164
+ | [Nebius](https://tokenfactory.nebius.com/) | $1 | Permanent |
165
+ | [SambaNova Cloud](https://cloud.sambanova.ai/) | $5 | 3 months |
166
+ | [AI21](https://studio.ai21.com/) | $10 | 3 months |
167
+ | [Upstage](https://console.upstage.ai/) | $10 | 3 months |
168
+ | [NLP Cloud](https://nlpcloud.com/home) | $15 | Permanent |
169
+ | [Alibaba DashScope](https://bailian.console.alibabacloud.com/) | 1M tokens/model | 90 days |
170
+ | [Scaleway](https://console.scaleway.com/generative-api/models) | 1M tokens | Permanent |
171
+ | [Modal](https://modal.com) | $5/month | Monthly |
172
+ | [Inference.net](https://inference.net) | $1 (+ $25 on survey) | Permanent |
173
+ | [Novita](https://novita.ai/) | $0.5 | 1 year |
174
+
175
+ ### 🎓 Free with Education/Developer Programs
176
+
177
+ | Program | What you get |
178
+ |---------|--------------|
179
+ | [GitHub Student Pack](https://education.github.com/pack) | Free Copilot Pro for students (verify with .edu email) |
180
+ | [GitHub Copilot Free](https://code.visualstudio.com/blogs/2024/12/18/free-github-copilot) | 50 chat + 2,000 completions/month in VS Code |
181
+ | [Copilot Pro for teachers/maintainers](https://docs.github.com/en/copilot/how-tos/manage-your-account/get-free-access-to-copilot-pro) | Free Copilot Pro for open source maintainers & educators |
182
+
183
+ ---
184
+
105
185
  ### Tier scale
106
186
 
107
187
  | Tier | SWE-bench | Best for |
@@ -369,6 +449,21 @@ For every model in this tool, **you own the generated output** — code, text, o
369
449
 
370
450
  ---
371
451
 
452
+ ## 📊 Telemetry
453
+
454
+ `free-coding-models` collects anonymous usage telemetry to help understand how the CLI is used and improve the product. No personal information, API keys, prompts, source code, file paths, or secrets are ever collected.
455
+
456
+ The telemetry payload is limited to anonymous product analytics such as the app version, selected tool mode, operating system, terminal family, and a random anonymous install ID stored locally on your machine. When a model is launched, telemetry can also include the selected tool, provider, model ID, model label, model tier, launch result, and a few product actions such as installing provider catalogs, saving/removing API keys, or toggling shell environment export.
457
+
458
+ Telemetry is enabled by default and can be disabled with any of the following:
459
+
460
+ | Method | How |
461
+ |--------|-----|
462
+ | CLI flag | Run `free-coding-models --no-telemetry` |
463
+ | Environment variable | Set `FREE_CODING_MODELS_TELEMETRY=0` (also supports `false` or `off`) |
464
+
465
+ ---
466
+
372
467
  ## 🛡️ Security & Trust
373
468
 
374
469
  ### Supply Chain
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.3.42",
3
+ "version": "0.3.44",
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",
package/src/app.js CHANGED
@@ -32,7 +32,7 @@
32
32
  * - `getTelemetryDistinctId`: Generate/reuse a stable anonymous ID for telemetry
33
33
  * - `getTelemetryTerminal`: Infer terminal family (Terminal.app, iTerm2, kitty, etc.)
34
34
  * - `isTelemetryDebugEnabled` / `telemetryDebug`: Optional runtime telemetry diagnostics via env
35
- * - `sendUsageTelemetry`: Fire-and-forget anonymous app-start event
35
+ * - `sendUsageTelemetry`: Fire-and-forget anonymous app-start, launch, and action events
36
36
  * - `ensureFavoritesConfig` / `toggleFavoriteModel`: Persist and toggle pinned favorites
37
37
  * - `promptApiKey`: Interactive wizard for first-time multi-provider API key setup
38
38
  * - `buildPingRequest` / `ping`: Build provider-specific probe requests and measure latency
@@ -271,6 +271,8 @@ export async function runApp(cliArgs, config) {
271
271
  })
272
272
  if (requestedMode) mode = requestedMode
273
273
 
274
+ const sessionId = `session_${randomUUID()}`
275
+
274
276
  // 📖 Track app opening early so fast exits are still counted.
275
277
  // 📖 Must run before update checks because npm registry lookups can add startup delay.
276
278
  void sendUsageTelemetry(config, cliArgs, {
@@ -278,6 +280,10 @@ export async function runApp(cliArgs, config) {
278
280
  version: LOCAL_VERSION,
279
281
  mode,
280
282
  ts: new Date().toISOString(),
283
+ properties: {
284
+ session_id: sessionId,
285
+ event_version: 1,
286
+ },
281
287
  })
282
288
 
283
289
  // 📖 Auto-update detection: check npm registry for new versions at startup.
@@ -419,6 +425,7 @@ export async function runApp(cliArgs, config) {
419
425
  originFilterMode: 0, // 📖 Index into ORIGIN_CYCLE (0=All, then providers)
420
426
  hideUnconfiguredModels: config.settings?.hideUnconfiguredModels === true, // 📖 Hide providers with no configured API key when true.
421
427
  favoritesPinnedAndSticky: config.settings?.favoritesPinnedAndSticky === true, // 📖 false by default: favorites follow normal sort/filter rules until Y enables pinned+sticky mode.
428
+ footerHidden: config.settings?.footerHidden === true, // 📖 true = footer is collapsed to a single toggle hint
422
429
  scrollOffset: 0, // 📖 First visible model index in viewport
423
430
  terminalRows: process.stdout.rows || 24, // 📖 Current terminal height
424
431
  terminalCols: process.stdout.columns || 80, // 📖 Current terminal width
@@ -438,6 +445,7 @@ export async function runApp(cliArgs, config) {
438
445
  settingsUpdateLatestVersion: null, // 📖 Latest npm version discovered from manual check
439
446
  settingsUpdateError: null, // 📖 Last update-check error message for maintenance row
440
447
  config, // 📖 Live reference to the config object (updated on save)
448
+ sessionId, // 📖 Per-process analytics link between app_start and later app_use/app_action events.
441
449
  visibleSorted: [], // 📖 Cached visible+sorted models — shared between render loop and key handlers
442
450
  commandPaletteOpen: false, // 📖 Whether the Ctrl+P command palette overlay is active.
443
451
  commandPaletteQuery: '', // 📖 Current command palette search query.
@@ -863,6 +871,7 @@ export async function runApp(cliArgs, config) {
863
871
  getToolInstallPlan,
864
872
  isToolInstalled,
865
873
  installToolWithPlan,
874
+ sendUsageTelemetry,
866
875
  startRecommendAnalysis: overlays.startRecommendAnalysis,
867
876
  stopRecommendAnalysis: overlays.stopRecommendAnalysis,
868
877
  sendBugReport,
@@ -1112,7 +1121,7 @@ export async function runApp(cliArgs, config) {
1112
1121
  pinFavorites: state.favoritesPinnedAndSticky,
1113
1122
  })
1114
1123
 
1115
- process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate))
1124
+ process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate, state.footerHidden))
1116
1125
  if (process.stdout.isTTY) {
1117
1126
  process.stdout.flush && process.stdout.flush()
1118
1127
  }
@@ -33,6 +33,7 @@ const TOOL_MODE_DESCRIPTIONS = {
33
33
  cline: 'Launch Cline CLI with the selected model.',
34
34
  rovo: 'Rovo Dev CLI model (launch with Rovo tool only).',
35
35
  gemini: 'Gemini CLI model (launch with Gemini tool only).',
36
+ jcode: 'Launch jcode coding agent with the selected model.',
36
37
  }
37
38
 
38
39
  const TOOL_MODE_COMMANDS = TOOL_MODE_ORDER.map((toolMode) => {
package/src/config.js CHANGED
@@ -212,6 +212,7 @@ function normalizeSettingsSection(settings) {
212
212
  hideUnconfiguredModels: typeof safeSettings.hideUnconfiguredModels === 'boolean' ? safeSettings.hideUnconfiguredModels : true,
213
213
  favoritesPinnedAndSticky: typeof safeSettings.favoritesPinnedAndSticky === 'boolean' ? safeSettings.favoritesPinnedAndSticky : false,
214
214
  theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : 'auto',
215
+ footerHidden: typeof safeSettings.footerHidden === 'boolean' ? safeSettings.footerHidden : false,
215
216
  }
216
217
  }
217
218
 
@@ -839,7 +840,7 @@ export function isProviderEnabled(config, providerKey) {
839
840
  /**
840
841
  * 📖 _emptyProfileSettings: Default TUI settings.
841
842
  *
842
- * @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number, hideUnconfiguredModels: boolean, favoritesPinnedAndSticky: boolean, preferredToolMode: string }}
843
+ * @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number, hideUnconfiguredModels: boolean, favoritesPinnedAndSticky: boolean, preferredToolMode: string, theme: string, footerHidden: boolean }}
843
844
  */
844
845
  export function _emptyProfileSettings() {
845
846
  return {
@@ -851,6 +852,7 @@ export function _emptyProfileSettings() {
851
852
  favoritesPinnedAndSticky: false, // 📖 default mode keeps favorites as normal starred rows; press Y to pin+stick them.
852
853
  preferredToolMode: 'opencode', // 📖 remember the last Z-selected launcher across app restarts
853
854
  theme: 'auto', // 📖 'auto' follows the terminal/OS theme, override with 'dark' or 'light' if needed
855
+ footerHidden: false, // 📖 false = full footer shown; true = collapsed to a single "(W) Toggle Footer" hint
854
856
  }
855
857
  }
856
858
 
@@ -11,6 +11,10 @@
11
11
  * 📖 Key I opens the unified "Feedback, bugs & requests" overlay.
12
12
  *
13
13
  * It also owns the "test key" model selection used by the Settings overlay.
14
+ * Anonymous telemetry hooks for model launches and a few high-signal settings
15
+ * actions live here too, because this module already sees the final effective
16
+ * tool mode, provider, and selected model right before the app hands control
17
+ * to an external CLI.
14
18
  * Some providers expose models in `/v1/models` that are not actually callable
15
19
  * on the chat-completions endpoint. To avoid false negatives when a user
16
20
  * presses `T` in Settings, the helpers below discover candidate model IDs,
@@ -195,6 +199,7 @@ export function createKeyHandler(ctx) {
195
199
  getToolInstallPlan,
196
200
  isToolInstalled,
197
201
  installToolWithPlan,
202
+ sendUsageTelemetry,
198
203
  startRecommendAnalysis,
199
204
  stopRecommendAnalysis,
200
205
  sendBugReport,
@@ -232,6 +237,56 @@ export function createKeyHandler(ctx) {
232
237
  return mode !== 'opencode-desktop'
233
238
  }
234
239
 
240
+ function getModelTelemetryFamily(providerKey) {
241
+ if (providerKey === 'rovo' || providerKey === 'gemini' || providerKey === 'opencode-zen') return providerKey
242
+ return 'standard'
243
+ }
244
+
245
+ function trackTelemetryEvent(event, properties = {}) {
246
+ if (typeof sendUsageTelemetry !== 'function') return
247
+ void sendUsageTelemetry(state.config, cliArgs, {
248
+ event,
249
+ mode: state.mode,
250
+ ts: new Date().toISOString(),
251
+ properties: {
252
+ session_id: state.sessionId,
253
+ event_version: 1,
254
+ ...properties,
255
+ },
256
+ })
257
+ }
258
+
259
+ function buildModelLaunchTelemetry(selected, extra = {}) {
260
+ return {
261
+ action_type: 'launch_model',
262
+ tool_mode: state.mode,
263
+ provider_key: selected.providerKey,
264
+ model_id: selected.modelId,
265
+ model_label: selected.label,
266
+ model_tier: selected.tier,
267
+ model_family: getModelTelemetryFamily(selected.providerKey),
268
+ ...extra,
269
+ }
270
+ }
271
+
272
+ function trackAppUse(selected, extra = {}) {
273
+ trackTelemetryEvent('app_use', buildModelLaunchTelemetry(selected, extra))
274
+ }
275
+
276
+ function trackAppUseResult(selected, launchResult, extra = {}) {
277
+ trackTelemetryEvent('app_use_result', buildModelLaunchTelemetry(selected, {
278
+ launch_result: launchResult,
279
+ ...extra,
280
+ }))
281
+ }
282
+
283
+ function trackAppAction(actionType, properties = {}) {
284
+ trackTelemetryEvent('app_action', {
285
+ action_type: actionType,
286
+ ...properties,
287
+ })
288
+ }
289
+
235
290
  async function launchSelectedModel(selected, options = {}) {
236
291
  const { uiAlreadyStopped = false } = options
237
292
  userSelected = { modelId: selected.modelId, label: selected.label, tier: selected.tier, providerKey: selected.providerKey }
@@ -264,6 +319,9 @@ export function createKeyHandler(ctx) {
264
319
 
265
320
  // 📖 Case A: User is in Rovo/Gemini mode but selected a model from a different provider
266
321
  if (isActiveModeCliOnly && !modelBelongsToActiveMode) {
322
+ trackAppUseResult(selected, 'blocked_incompatible_model', {
323
+ blocked_by_tool_mode: state.mode,
324
+ })
267
325
  const availableModels = MODELS.filter(m => m[5] === state.mode)
268
326
  console.log(chalk.yellow(` ⚠ ${activeMeta.label} can only launch its own models.`))
269
327
  console.log(chalk.yellow(` "${selected.label}" is not a ${activeMeta.label} model.`))
@@ -331,6 +389,9 @@ export function createKeyHandler(ctx) {
331
389
 
332
390
  const installResult = await installToolWithPlan(installPlan)
333
391
  if (!installResult.ok) {
392
+ trackAppUseResult(selected, 'blocked_missing_tool', {
393
+ required_tool_mode: toolModeForProvider,
394
+ })
334
395
  console.log(chalk.red(` X Tool installation failed with exit code ${installResult.exitCode}.`))
335
396
  if (installPlan.docsUrl) console.log(chalk.dim(` Docs: ${installPlan.docsUrl}`))
336
397
  console.log()
@@ -339,6 +400,9 @@ export function createKeyHandler(ctx) {
339
400
 
340
401
  // 📖 Verify tool is now installed
341
402
  if (!isToolInstalled(toolModeForProvider)) {
403
+ trackAppUseResult(selected, 'blocked_missing_tool', {
404
+ required_tool_mode: toolModeForProvider,
405
+ })
342
406
  console.log(chalk.yellow(' ⚠ The installer finished, but the tool is still not reachable from this terminal session.'))
343
407
  console.log(chalk.dim(' Restart your shell or add the tool bin directory to PATH, then retry the launch.'))
344
408
  if (installPlan.docsUrl) console.log(chalk.dim(` Docs: ${installPlan.docsUrl}`))
@@ -351,6 +415,10 @@ export function createKeyHandler(ctx) {
351
415
  }
352
416
  }
353
417
 
418
+ const launchSource = uiAlreadyStopped ? 'tool_install_retry' : 'tui_enter'
419
+ trackAppUse(selected, { launch_source: launchSource })
420
+ trackAppUseResult(selected, 'started', { launch_source: launchSource })
421
+
354
422
  let exitCode = 0
355
423
  if (state.mode === 'openclaw') {
356
424
  exitCode = await startOpenClaw(userSelected, state.config, { launchCli: true })
@@ -377,6 +445,9 @@ export function createKeyHandler(ctx) {
377
445
 
378
446
  const installResult = await installToolWithPlan(currentPlan)
379
447
  if (!installResult.ok) {
448
+ trackAppUseResult(selected, 'blocked_missing_tool', {
449
+ required_tool_mode: state.mode,
450
+ })
380
451
  console.log(chalk.red(` X Tool installation failed with exit code ${installResult.exitCode}.`))
381
452
  if (currentPlan?.docsUrl) console.log(chalk.dim(` Docs: ${currentPlan.docsUrl}`))
382
453
  console.log()
@@ -384,6 +455,9 @@ export function createKeyHandler(ctx) {
384
455
  }
385
456
 
386
457
  if (shouldCheckMissingTool(state.mode) && !isToolInstalled(state.mode)) {
458
+ trackAppUseResult(selected, 'blocked_missing_tool', {
459
+ required_tool_mode: state.mode,
460
+ })
387
461
  console.log(chalk.yellow(' ⚠ The installer finished, but the tool is still not reachable from this terminal session.'))
388
462
  console.log(chalk.dim(' Restart your shell or add the tool bin directory to PATH, then retry the launch.'))
389
463
  if (currentPlan?.docsUrl) console.log(chalk.dim(` Docs: ${currentPlan.docsUrl}`))
@@ -565,6 +639,9 @@ export function createKeyHandler(ctx) {
565
639
  } else {
566
640
  removeShellEnv()
567
641
  }
642
+ trackAppAction('shell_env_export_toggled', {
643
+ enabled: state.config.settings.shellEnvEnabled === true,
644
+ })
568
645
  }
569
646
 
570
647
  function resetInstallEndpointsOverlay() {
@@ -605,6 +682,14 @@ export function createKeyHandler(ctx) {
605
682
  ...(result.extraPath ? [chalk.bold(`Secrets:`) + ` ${result.extraPath}`] : []),
606
683
  ],
607
684
  }
685
+ trackAppAction('install_provider_endpoints', {
686
+ provider_key: result.providerKey,
687
+ tool_mode: result.toolMode,
688
+ install_scope: result.scope,
689
+ connection_mode: state.installEndpointsConnectionMode || 'direct',
690
+ model_count: result.modelCount,
691
+ selected_model_count: selectedModelIds.length,
692
+ })
608
693
  state.installEndpointsPhase = 'result'
609
694
  state.installEndpointsCursor = 0
610
695
  state.installEndpointsScrollOffset = 0
@@ -1880,6 +1965,11 @@ export function createKeyHandler(ctx) {
1880
1965
  if (!saveResult.success) {
1881
1966
  state.settingsErrorMsg = `⚠️ Failed to persist ${pk} API key: ${saveResult.error || 'Unknown error'}`
1882
1967
  setTimeout(() => { state.settingsErrorMsg = null }, 4000)
1968
+ } else {
1969
+ trackAppAction('api_key_saved', {
1970
+ provider_key: pk,
1971
+ key_action: state.settingsAddKeyMode ? 'add' : 'replace',
1972
+ })
1883
1973
  }
1884
1974
  }
1885
1975
  state.settingsEditMode = false
@@ -2111,6 +2201,10 @@ export function createKeyHandler(ctx) {
2111
2201
  ? `✅ Removed one key for ${pk} (${remaining} remaining)`
2112
2202
  : `✅ Removed last API key for ${pk}`
2113
2203
  state.settingsSyncStatus = { type: 'success', msg }
2204
+ trackAppAction('api_key_removed', {
2205
+ provider_key: pk,
2206
+ remaining_key_count: remaining,
2207
+ })
2114
2208
  }
2115
2209
  return
2116
2210
  }
@@ -2191,10 +2285,20 @@ export function createKeyHandler(ctx) {
2191
2285
  // 📖 W cycles the supported ping modes:
2192
2286
  // 📖 speed (2s) → normal (10s) → slow (30s) → forced (4s) → speed.
2193
2287
  // 📖 forced ignores auto speed/slow transitions until the user leaves it manually.
2194
- if (key.name === 'w') {
2288
+ if (key.name === 'w' && !key.alt && !key.ctrl && !key.meta) {
2195
2289
  const currentIdx = PING_MODE_CYCLE.indexOf(state.pingMode)
2196
2290
  const nextIdx = currentIdx >= 0 ? (currentIdx + 1) % PING_MODE_CYCLE.length : 0
2197
2291
  setPingMode(PING_MODE_CYCLE[nextIdx], 'manual')
2292
+ return
2293
+ }
2294
+
2295
+ // 📖 Alt+W: toggle footer visibility (collapse to single hint when hidden)
2296
+ if (key.name === 'w' && key.alt && !key.ctrl && !key.meta) {
2297
+ state.footerHidden = !state.footerHidden
2298
+ if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
2299
+ state.config.settings.footerHidden = state.footerHidden
2300
+ saveConfig(state.config)
2301
+ return
2198
2302
  }
2199
2303
 
2200
2304
  // 📖 E toggles hiding models whose provider has no configured API key.
package/src/openclaw.js CHANGED
@@ -33,6 +33,8 @@ import { installProviderEndpoints } from './endpoint-installer.js'
33
33
  import { ENV_VAR_NAMES } from './provider-metadata.js'
34
34
  import { PROVIDER_COLOR } from './render-table.js'
35
35
  import { resolveToolBinaryPath } from './tool-bootstrap.js'
36
+ import { getApiKey } from './config.js'
37
+ import { syncShellEnv } from './shell-env.js'
36
38
 
37
39
  const OPENCLAW_CONFIG = join(homedir(), '.openclaw', 'openclaw.json')
38
40
 
@@ -56,7 +58,14 @@ export function saveOpenClawConfig(config, options = {}) {
56
58
  writeFileSync(filePath, JSON.stringify(config, null, 2))
57
59
  }
58
60
 
59
- function spawnOpenClawCli() {
61
+ /**
62
+ * 📖 Spawn OpenClaw CLI with proper environment including API key.
63
+ * OpenClaw reads API keys from environment variables at runtime, not just config file.
64
+ *
65
+ * @param {NodeJS.ProcessEnv} env - Environment variables including API key
66
+ * @returns {Promise<number>} Exit code
67
+ */
68
+ function spawnOpenClawCli(env = process.env) {
60
69
  return new Promise(async (resolve, reject) => {
61
70
  const { spawn } = await import('child_process')
62
71
  const command = resolveToolBinaryPath('openclaw') || 'openclaw'
@@ -64,7 +73,7 @@ function spawnOpenClawCli() {
64
73
  stdio: 'inherit',
65
74
  shell: false,
66
75
  detached: false,
67
- env: process.env,
76
+ env,
68
77
  })
69
78
 
70
79
  child.on('exit', (code) => resolve(typeof code === 'number' ? code : 0))
@@ -107,16 +116,29 @@ export async function startOpenClaw(model, config, options = {}) {
107
116
  })
108
117
 
109
118
  const providerEnvName = ENV_VAR_NAMES[model.providerKey]
119
+ const apiKey = getApiKey(config, model.providerKey)
110
120
  console.log(chalk.rgb(255, 140, 0)(` ✓ Default model set to: ${result.primaryModelRef || `${result.providerId}/${model.modelId}`}`))
111
121
  console.log()
112
122
  console.log(chalk.dim(` 📄 Config updated: ${result.path}`))
113
123
  if (result.backupPath) console.log(chalk.dim(` 💾 Backup: ${result.backupPath}`))
114
124
  if (providerEnvName) console.log(chalk.dim(` 🔑 API key synced under config env.${providerEnvName}`))
115
125
  console.log()
126
+
116
127
  if (options.launchCli) {
128
+ // 📖 Sync shell env so API key is available as environment variable
129
+ if (config.settings?.shellEnvEnabled !== false) {
130
+ syncShellEnv(config)
131
+ }
132
+
133
+ // 📖 Build env with API key so OpenClaw can authenticate
134
+ const launchEnv = { ...process.env }
135
+ if (apiKey && providerEnvName) {
136
+ launchEnv[providerEnvName] = apiKey
137
+ }
138
+
117
139
  console.log(chalk.dim(' Starting OpenClaw...'))
118
140
  console.log()
119
- await spawnOpenClawCli()
141
+ await spawnOpenClawCli(launchEnv)
120
142
  } else {
121
143
  console.log(chalk.dim(' 💡 OpenClaw will reload config automatically when it notices the file change.'))
122
144
  console.log(chalk.dim(` To apply manually: openclaw models set ${result.primaryModelRef || `${result.providerId}/${model.modelId}`}`))
package/src/opencode.js CHANGED
@@ -465,6 +465,20 @@ export async function startOpenCode(model, fcmConfig) {
465
465
  options: { baseURL: 'https://apis.iflow.cn/v1', apiKey: '{env:IFLOW_API_KEY}' },
466
466
  models: {}
467
467
  }
468
+ } else if (providerKey === 'chutes') {
469
+ config.provider.chutes = {
470
+ npm: '@ai-sdk/openai-compatible',
471
+ name: 'Chutes AI',
472
+ options: { baseURL: 'https://chutes.ai/v1', apiKey: '{env:CHUTES_API_KEY}' },
473
+ models: {}
474
+ }
475
+ } else if (providerKey === 'ovhcloud') {
476
+ config.provider.ovhcloud = {
477
+ npm: '@ai-sdk/openai-compatible',
478
+ name: 'OVHcloud AI',
479
+ options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
480
+ models: {}
481
+ }
468
482
  }
469
483
  }
470
484
 
@@ -724,6 +738,20 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
724
738
  options: { baseURL: 'https://apis.iflow.cn/v1', apiKey: '{env:IFLOW_API_KEY}' },
725
739
  models: {}
726
740
  }
741
+ } else if (providerKey === 'chutes') {
742
+ config.provider.chutes = {
743
+ npm: '@ai-sdk/openai-compatible',
744
+ name: 'Chutes AI',
745
+ options: { baseURL: 'https://chutes.ai/v1', apiKey: '{env:CHUTES_API_KEY}' },
746
+ models: {}
747
+ }
748
+ } else if (providerKey === 'ovhcloud') {
749
+ config.provider.ovhcloud = {
750
+ npm: '@ai-sdk/openai-compatible',
751
+ name: 'OVHcloud AI',
752
+ options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
753
+ models: {}
754
+ }
727
755
  }
728
756
  }
729
757
 
@@ -104,7 +104,7 @@ export const PROVIDER_COLOR = new Proxy({}, {
104
104
  })
105
105
 
106
106
  // ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
107
- export 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, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null) {
107
+ export 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, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null, footerHidden = false) {
108
108
  // 📖 Filter out hidden models for display
109
109
  const visibleResults = results.filter(r => !r.hidden)
110
110
 
@@ -780,7 +780,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
780
780
 
781
781
  // 📖 Line 2: command palette, recommend, feedback, theme
782
782
  {
783
- const cpText = ' NEW ! CTRL+P ⚡️ Command Palette '
783
+ const cpText = ' CTRL+P ⚡️ Command Palette '
784
784
  const parts = [
785
785
  { text: ' ', key: null },
786
786
  { text: cpText, key: 'ctrl+p' },
@@ -802,7 +802,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
802
802
 
803
803
  // 📖 Line 2: command palette (highlighted as new), recommend, feedback, and extended hints.
804
804
  // 📖 CTRL+P ⚡️ Command Palette uses neon-green-on-dark-green background to highlight the feature.
805
- const paletteLabel = chalk.bgRgb(0, 60, 0).rgb(57, 255, 20).bold(' NEW ! CTRL+P ⚡️ Command Palette ')
805
+ const paletteLabel = chalk.bgRgb(0, 60, 0).rgb(57, 255, 20).bold(' CTRL+P ⚡️ Command Palette ')
806
806
  lines.push(
807
807
  ' ' + paletteLabel + themeColors.dim(` • `) +
808
808
  hotkey('Q', ' Smart Recommend') + themeColors.dim(` • `) +
@@ -878,27 +878,35 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
878
878
 
879
879
  _lastLayout.footerHotkeys = footerHotkeys
880
880
 
881
- const releaseLabel = lastReleaseDate
882
- ? chalk.rgb(255, 182, 193)(`Last release: ${lastReleaseDate}`)
883
- : ''
884
-
885
- lines.push(
886
- ' ' + themeColors.hotkey('N') + themeColors.dim(' Changelog') +
887
- (filterBadge
888
- ? themeColors.dim(' • ') + filterBadge
889
- : '') +
890
- themeColors.dim('') +
891
- themeColors.dim('Ctrl+C Exit') +
892
- (releaseLabel ? themeColors.dim(' • ') + releaseLabel : '')
893
- )
894
-
895
- // 📖 Discord link at the very bottom of the TUI
896
- lines.push(
897
- ' 💬 ' +
898
- themeColors.footerDiscord('\x1b]8;;https://discord.gg/ZTNFHvvCkU\x1b\\Join the Discord\x1b]8;;\x1b\\') +
899
- themeColors.dim(' ') +
900
- themeColors.footerDiscord('https://discord.gg/ZTNFHvvCkU')
901
- )
881
+ if (footerHidden) {
882
+ // 📖 Collapsed footer: single line with toggle hint
883
+ lines.push(
884
+ ' ' + themeColors.hotkey('Alt+W') + themeColors.dim(' Toggle Footer') +
885
+ themeColors.dim(' • Ctrl+C Exit')
886
+ )
887
+ } else {
888
+ const releaseLabel = lastReleaseDate
889
+ ? chalk.rgb(255, 182, 193)(`Last release: ${lastReleaseDate}`)
890
+ : ''
891
+
892
+ lines.push(
893
+ ' ' + themeColors.hotkey('N') + themeColors.dim(' Changelog') +
894
+ (filterBadge
895
+ ? themeColors.dim(' • ') + filterBadge
896
+ : '') +
897
+ themeColors.dim(' ') +
898
+ themeColors.dim('Ctrl+C Exit') +
899
+ (releaseLabel ? themeColors.dim(' • ') + releaseLabel : '')
900
+ )
901
+
902
+ // 📖 Discord link at the very bottom of the TUI
903
+ lines.push(
904
+ ' 💬 ' +
905
+ themeColors.footerDiscord('\x1b]8;;https://discord.gg/ZTNFHvvCkU\x1b\\Join the Discord\x1b]8;;\x1b\\') +
906
+ themeColors.dim(' → ') +
907
+ themeColors.footerDiscord('https://discord.gg/ZTNFHvvCkU')
908
+ )
909
+ }
902
910
 
903
911
  // 📖 Append \x1b[K (erase to EOL) to each line so leftover chars from previous
904
912
  // 📖 frames are cleared. Then pad with blank cleared lines to fill the terminal,