free-coding-models 0.3.43 → 0.3.45

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,13 +1,12 @@
1
- ## [0.3.43] - 2026-04-09
1
+ ## [0.3.45] - 2026-04-11
2
2
 
3
3
  ### Added
4
- - Added new anonymous telemetry events for real product usage: `app_use` records which tool, provider, and model users actually launch, while `app_action` tracks a small set of high-signal in-app actions such as endpoint installs, API key save/remove flows, and shell env export toggles.
5
- - Added per-session telemetry linking with a generated `session_id`, so `app_start`, `app_use`, and action events can be analyzed together while still keeping users anonymous through the existing persistent install-scoped `distinct_id`.
6
- - Added launch outcome tracking with `app_use_result`, so the dashboard can distinguish successful launches from blocked flows like incompatible model/tool combinations or missing CLI installs.
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
+ - Added **Alt+W** footer toggle collapses the footer to a single-line hint showing only the toggle hotkey and exit command, giving more vertical space for the model table.
7
6
 
8
7
  ### Changed
9
- - Extended the telemetry pipeline to support custom event properties on top of the shared anonymous metadata (`app`, `version`, `mode`, `system`, `terminal`) without introducing a second analytics path.
10
- - Updated the README telemetry section to explain that anonymous analytics now cover launched tool/provider/model metadata and a few product actions, while still explicitly excluding prompts, source code, file paths, API keys, and secrets.
8
+ - 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.
9
+ - Command Palette label updated to remove "NEW !" badge the feature is no longer new.
11
10
 
12
11
  ### Fixed
13
- - Added regression coverage for telemetry opt-out behavior and event payload shape, ensuring the new launch/action events stay disabled under `--no-telemetry` or `FREE_CODING_MODELS_TELEMETRY=0`.
12
+ - 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,6 +34,7 @@ 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> •
@@ -103,6 +104,84 @@ Create a free account on one provider below to get started:
103
104
 
104
105
  > 💡 One key is enough. Add more at any time with **`P`** inside the TUI.
105
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
+
106
185
  ### Tier scale
107
186
 
108
187
  | Tier | SWE-bench | Best for |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.3.43",
3
+ "version": "0.3.45",
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
@@ -240,7 +240,7 @@ export async function runApp(cliArgs, config) {
240
240
  config.settings.shellEnvEnabled = false
241
241
  saveConfig(config)
242
242
  }
243
- // 📖 'skip' leaves shellEnvEnabled undefinedwill prompt again next launch
243
+ // 📖 'skip' (Ctrl+C) now also sets shellEnvEnabled = false — prompt won't reappear
244
244
  }
245
245
 
246
246
  // 📖 Default mode: use the last persisted launcher choice when valid,
@@ -425,6 +425,7 @@ export async function runApp(cliArgs, config) {
425
425
  originFilterMode: 0, // 📖 Index into ORIGIN_CYCLE (0=All, then providers)
426
426
  hideUnconfiguredModels: config.settings?.hideUnconfiguredModels === true, // 📖 Hide providers with no configured API key when true.
427
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
428
429
  scrollOffset: 0, // 📖 First visible model index in viewport
429
430
  terminalRows: process.stdout.rows || 24, // 📖 Current terminal height
430
431
  terminalCols: process.stdout.columns || 80, // 📖 Current terminal width
@@ -1120,7 +1121,7 @@ export async function runApp(cliArgs, config) {
1120
1121
  pinFavorites: state.favoritesPinnedAndSticky,
1121
1122
  })
1122
1123
 
1123
- 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))
1124
1125
  if (process.stdout.isTTY) {
1125
1126
  process.stdout.flush && process.stdout.flush()
1126
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
 
@@ -2285,10 +2285,20 @@ export function createKeyHandler(ctx) {
2285
2285
  // 📖 W cycles the supported ping modes:
2286
2286
  // 📖 speed (2s) → normal (10s) → slow (30s) → forced (4s) → speed.
2287
2287
  // 📖 forced ignores auto speed/slow transitions until the user leaves it manually.
2288
- if (key.name === 'w') {
2288
+ if (key.name === 'w' && !key.alt && !key.ctrl && !key.meta) {
2289
2289
  const currentIdx = PING_MODE_CYCLE.indexOf(state.pingMode)
2290
2290
  const nextIdx = currentIdx >= 0 ? (currentIdx + 1) % PING_MODE_CYCLE.length : 0
2291
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
2292
2302
  }
2293
2303
 
2294
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
@@ -337,6 +337,41 @@ export async function startOpenCode(model, fcmConfig) {
337
337
  return
338
338
  }
339
339
 
340
+ // 📖 Zen models are built-in to OpenCode — they use the native `opencode` provider prefix
341
+ // 📖 and don't need a custom provider entry in opencode.json.
342
+ if (providerKey === 'opencode-zen') {
343
+ const zenModelRef = `opencode/${ocModelId}`
344
+ console.log(chalk.green(` Setting ${chalk.bold(model.label)} as default (Zen built-in)...`))
345
+ console.log(chalk.dim(` Model: ${zenModelRef}`))
346
+ console.log()
347
+
348
+ const config = loadOpenCodeConfig()
349
+ const backupPath = `${getOpenCodeConfigPath()}.backup-${Date.now()}`
350
+
351
+ if (existsSync(getOpenCodeConfigPath())) {
352
+ copyFileSync(getOpenCodeConfigPath(), backupPath)
353
+ console.log(chalk.dim(` Backup: ${backupPath}`))
354
+ }
355
+
356
+ config.model = zenModelRef
357
+ saveOpenCodeConfig(config)
358
+
359
+ const savedConfig = loadOpenCodeConfig()
360
+ console.log(chalk.dim(` Config saved to: ${getOpenCodeConfigPath()}`))
361
+ console.log(chalk.dim(` Default model in config: ${savedConfig.model || 'NOT SET'}`))
362
+ console.log()
363
+
364
+ if (savedConfig.model === config.model) {
365
+ console.log(chalk.green(` Default model set to: ${zenModelRef}`))
366
+ } else {
367
+ console.log(chalk.yellow(` Config might not have been saved correctly`))
368
+ }
369
+ console.log()
370
+
371
+ await spawnOpenCode(['--model', zenModelRef], providerKey, fcmConfig)
372
+ return
373
+ }
374
+
340
375
  console.log(chalk.green(` Setting ${chalk.bold(model.label)} as default...`))
341
376
  console.log(chalk.dim(` Model: ${modelRef}`))
342
377
  console.log()
@@ -465,6 +500,20 @@ export async function startOpenCode(model, fcmConfig) {
465
500
  options: { baseURL: 'https://apis.iflow.cn/v1', apiKey: '{env:IFLOW_API_KEY}' },
466
501
  models: {}
467
502
  }
503
+ } else if (providerKey === 'chutes') {
504
+ config.provider.chutes = {
505
+ npm: '@ai-sdk/openai-compatible',
506
+ name: 'Chutes AI',
507
+ options: { baseURL: 'https://chutes.ai/v1', apiKey: '{env:CHUTES_API_KEY}' },
508
+ models: {}
509
+ }
510
+ } else if (providerKey === 'ovhcloud') {
511
+ config.provider.ovhcloud = {
512
+ npm: '@ai-sdk/openai-compatible',
513
+ name: 'OVHcloud AI',
514
+ options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
515
+ models: {}
516
+ }
468
517
  }
469
518
  }
470
519
 
@@ -596,6 +645,42 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
596
645
  return
597
646
  }
598
647
 
648
+ // 📖 Zen models are built-in to OpenCode — remap to `opencode/<model-id>` and skip provider config.
649
+ if (providerKey === 'opencode-zen') {
650
+ const zenModelRef = `opencode/${ocModelId}`
651
+ console.log(chalk.green(` Setting ${chalk.bold(model.label)} as default for OpenCode Desktop (Zen built-in)...`))
652
+ console.log(chalk.dim(` Model: ${zenModelRef}`))
653
+ console.log()
654
+
655
+ const config = loadOpenCodeConfig()
656
+ const backupPath = `${getOpenCodeConfigPath()}.backup-${Date.now()}`
657
+
658
+ if (existsSync(getOpenCodeConfigPath())) {
659
+ copyFileSync(getOpenCodeConfigPath(), backupPath)
660
+ console.log(chalk.dim(` Backup: ${backupPath}`))
661
+ }
662
+
663
+ config.model = zenModelRef
664
+ saveOpenCodeConfig(config)
665
+
666
+ const savedConfig = loadOpenCodeConfig()
667
+ console.log(chalk.dim(` Config saved to: ${getOpenCodeConfigPath()}`))
668
+ console.log(chalk.dim(` Default model in config: ${savedConfig.model || 'NOT SET'}`))
669
+ console.log()
670
+
671
+ if (savedConfig.model === config.model) {
672
+ console.log(chalk.green(` Default model set to: ${zenModelRef}`))
673
+ } else {
674
+ console.log(chalk.yellow(` Config might not have been saved correctly`))
675
+ }
676
+ console.log()
677
+ console.log(chalk.dim(' Opening OpenCode Desktop...'))
678
+ console.log()
679
+
680
+ await launchDesktop()
681
+ return
682
+ }
683
+
599
684
  console.log(chalk.green(` Setting ${chalk.bold(model.label)} as default for OpenCode Desktop...`))
600
685
  console.log(chalk.dim(` Model: ${modelRef}`))
601
686
  console.log()
@@ -724,6 +809,20 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
724
809
  options: { baseURL: 'https://apis.iflow.cn/v1', apiKey: '{env:IFLOW_API_KEY}' },
725
810
  models: {}
726
811
  }
812
+ } else if (providerKey === 'chutes') {
813
+ config.provider.chutes = {
814
+ npm: '@ai-sdk/openai-compatible',
815
+ name: 'Chutes AI',
816
+ options: { baseURL: 'https://chutes.ai/v1', apiKey: '{env:CHUTES_API_KEY}' },
817
+ models: {}
818
+ }
819
+ } else if (providerKey === 'ovhcloud') {
820
+ config.provider.ovhcloud = {
821
+ npm: '@ai-sdk/openai-compatible',
822
+ name: 'OVHcloud AI',
823
+ options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
824
+ models: {}
825
+ }
727
826
  }
728
827
  }
729
828
 
@@ -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,
package/src/shell-env.js CHANGED
@@ -368,6 +368,10 @@ export async function promptShellEnvMigration(config) {
368
368
  if (key.ctrl && key.name === 'c') {
369
369
  if (process.stdin.isTTY) process.stdin.setRawMode(false)
370
370
  process.stdin.removeListener('keypress', onKey)
371
+ // 📖 User chose to skip — record this so the prompt never shows again on restart
372
+ if (!config.settings) config.settings = {}
373
+ config.settings.shellEnvEnabled = false
374
+ saveConfig(config)
371
375
  resolve('skip')
372
376
  return
373
377
  }
@@ -280,6 +280,20 @@ export const TOOL_BOOTSTRAP_METADATA = {
280
280
  },
281
281
  },
282
282
  },
283
+ jcode: {
284
+ binary: 'jcode',
285
+ docsUrl: 'https://github.com/1jehuang/jcode',
286
+ install: {
287
+ default: {
288
+ shellCommand: 'curl -fsSL https://raw.githubusercontent.com/1jehuang/jcode/master/scripts/install.sh | bash',
289
+ summary: 'Install jcode via the official installer script.',
290
+ },
291
+ win32: {
292
+ shellCommand: 'irm https://raw.githubusercontent.com/1jehuang/jcode/master/scripts/install.ps1 | iex',
293
+ summary: 'Install jcode via the official PowerShell installer.',
294
+ },
295
+ },
296
+ },
283
297
  }
284
298
 
285
299
  export function getToolBootstrapMeta(mode) {
@@ -272,7 +272,7 @@ function writePiConfig(model, apiKey, baseUrl, paths = getDefaultToolPaths()) {
272
272
  const modelsBackupPath = backupIfExists(modelsFilePath)
273
273
  const modelsConfig = readJson(modelsFilePath, { providers: {} })
274
274
  if (!modelsConfig.providers || typeof modelsConfig.providers !== 'object') modelsConfig.providers = {}
275
- modelsConfig.providers.freeCodingModels = {
275
+ modelsConfig.providers[model.providerKey] = {
276
276
  baseUrl,
277
277
  api: 'openai-completions',
278
278
  apiKey,
@@ -284,7 +284,7 @@ function writePiConfig(model, apiKey, baseUrl, paths = getDefaultToolPaths()) {
284
284
  const settingsFilePath = paths.piSettingsPath
285
285
  const settingsBackupPath = backupIfExists(settingsFilePath)
286
286
  const settingsConfig = readJson(settingsFilePath, {})
287
- settingsConfig.defaultProvider = 'freeCodingModels'
287
+ settingsConfig.defaultProvider = model.providerKey
288
288
  settingsConfig.defaultModel = model.modelId
289
289
  writeJson(settingsFilePath, settingsConfig)
290
290
 
@@ -667,7 +667,7 @@ export function prepareExternalToolLaunch(mode, model, config, options = {}) {
667
667
  const result = writePiConfig(model, apiKey, baseUrl, paths)
668
668
  return {
669
669
  command: 'pi',
670
- args: ['--provider', 'freeCodingModels', '--model', model.modelId, '--api-key', apiKey],
670
+ args: ['--provider', model.providerKey, '--model', model.modelId, '--api-key', apiKey],
671
671
  env,
672
672
  apiKey,
673
673
  baseUrl,
@@ -758,6 +758,34 @@ export function prepareExternalToolLaunch(mode, model, config, options = {}) {
758
758
  }
759
759
  }
760
760
 
761
+ if (mode === 'jcode') {
762
+ if (model.providerKey === 'nvidia') {
763
+ const jcodeModelId = model.modelId.replace(/^nvidia\//, '').replace(/^openai\//, '')
764
+ const nvidiaBaseUrl = 'https://integrate.api.nvidia.com/v1'
765
+ const nvidiaEnv = { ...env, OPENAI_BASE_URL: nvidiaBaseUrl, OPENAI_API_BASE: nvidiaBaseUrl }
766
+ console.log(chalk.dim(` 📖 jcode will use provider: openai-compatible / model: ${jcodeModelId}`))
767
+ return {
768
+ command: 'jcode',
769
+ args: ['repl', '--provider', 'openai-compatible', '--model', jcodeModelId],
770
+ env: nvidiaEnv,
771
+ apiKey,
772
+ baseUrl: nvidiaBaseUrl,
773
+ meta,
774
+ configArtifacts: [],
775
+ }
776
+ }
777
+ console.log(chalk.dim(` 📖 jcode will use provider: ${model.providerKey} / model: ${model.modelId}`))
778
+ return {
779
+ command: 'jcode',
780
+ args: ['repl', '--provider', model.providerKey, '--model', model.modelId],
781
+ env,
782
+ apiKey,
783
+ baseUrl,
784
+ meta,
785
+ configArtifacts: [],
786
+ }
787
+ }
788
+
761
789
  return {
762
790
  blocked: true,
763
791
  exitCode: 1,
@@ -854,6 +882,11 @@ export async function startExternalTool(mode, model, config) {
854
882
  return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
855
883
  }
856
884
 
885
+ if (mode === 'jcode') {
886
+ console.log(chalk.dim(` 📖 Launching jcode...`))
887
+ return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
888
+ }
889
+
857
890
  console.log(chalk.red(` X Unsupported external tool mode: ${mode}`))
858
891
  return 1
859
892
  }
@@ -41,6 +41,7 @@ export const TOOL_METADATA = {
41
41
  cline: { label: 'Cline', emoji: '🧠', flag: '--cline', color: [100, 220, 180] },
42
42
  rovo: { label: 'Rovo Dev CLI', emoji: '🦘', flag: '--rovo', color: [148, 163, 184], cliOnly: true },
43
43
  gemini: { label: 'Gemini CLI', emoji: '♊', flag: '--gemini', color: [66, 165, 245], cliOnly: true },
44
+ jcode: { label: 'jcode', emoji: '🪼', flag: '--jcode', color: [255, 140, 0] },
44
45
  xcode: { label: 'Xcode Intelligence',emoji: '🛠️', flag: '--xcode', color: [20, 126, 251] },
45
46
  }
46
47
 
@@ -62,16 +63,17 @@ export const COMPAT_COLUMN_SLOTS = [
62
63
  { emoji: '🧠', toolKeys: ['cline'], color: [100, 220, 180] },
63
64
  { emoji: '🦘', toolKeys: ['rovo'], color: [148, 163, 184] },
64
65
  { emoji: '♊', toolKeys: ['gemini'], color: [66, 165, 245] },
66
+ { emoji: '🪼', toolKeys: ['jcode'], color: [255, 140, 0] },
65
67
  { emoji: '🛠️', toolKeys: ['xcode'], color: [20, 126, 251] },
66
68
  ]
67
69
 
68
70
  export const TOOL_MODE_ORDER = [
69
71
  'opencode',
72
+ 'pi',
70
73
  'opencode-desktop',
71
74
  'openclaw',
72
75
  'crush',
73
76
  'goose',
74
- 'pi',
75
77
  'aider',
76
78
  'qwen',
77
79
  'openhands',
@@ -82,6 +84,7 @@ export const TOOL_MODE_ORDER = [
82
84
  'xcode',
83
85
  'rovo',
84
86
  'gemini',
87
+ 'jcode',
85
88
  ]
86
89
 
87
90
  export function getToolMeta(mode) {