free-coding-models 0.1.87 โ†’ 0.2.0

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 CHANGED
@@ -67,7 +67,7 @@
67
67
 
68
68
  - **๐ŸŽฏ Coding-focused** โ€” Only LLM models optimized for code generation, not chat or vision
69
69
  - **๐ŸŒ Multi-provider** โ€” Models from NVIDIA NIM, Groq, Cerebras, SambaNova, OpenRouter, Hugging Face Inference, Replicate, DeepInfra, Fireworks AI, Codestral, Hyperbolic, Scaleway, Google AI, SiliconFlow, Together AI, Cloudflare Workers AI, Perplexity API, Alibaba Cloud (DashScope), ZAI, and iFlow
70
- - **โš™๏ธ Settings screen** โ€” Press `P` to manage provider API keys, enable/disable providers, test keys live, and manually check/install updates
70
+ - **โš™๏ธ Settings screen** โ€” Press `P` to manage provider API keys, enable/disable providers, configure the proxy, clean OpenCode proxy sync, and manually check/install updates
71
71
  - **๐Ÿ”€ Multi-account Proxy (`fcm-proxy`)** โ€” Automatically starts a local reverse proxy that groups all your accounts into a single provider in OpenCode; supports multi-account rotation and auto-detects usage limits to swap between providers.
72
72
  - **๐Ÿš€ Parallel pings** โ€” All models tested simultaneously via native `fetch`
73
73
  - **๐Ÿ“Š Real-time animation** โ€” Watch latency appear live in alternate screen buffer
@@ -77,13 +77,13 @@
77
77
  - **๐Ÿ“Š Uptime tracking** โ€” Percentage of successful pings shown in real-time
78
78
  - **๐Ÿ“ Stability score** โ€” Composite 0โ€“100 score measuring consistency (p95, jitter, spikes, uptime)
79
79
  - **๐Ÿ“Š Usage tracking** โ€” Monitor remaining quota for each exact provider/model pair when the provider exposes it; otherwise the TUI shows a green dot instead of a misleading percentage.
80
- - **๐Ÿ“œ Live Log Viewer** โ€” Press `X` to view real-time activity and error logs in a focused TUI overlay.
80
+ - **๐Ÿ“œ Request Log Overlay** โ€” Press `X` to inspect recent proxied requests and token usage for exact provider/model pairs.
81
81
  - **๐Ÿ›  MODEL_NOT_FOUND Rotation** โ€” If a specific provider returns a 404 for a model, the TUI intelligently rotates through other available providers for the same model.
82
82
  - **๐Ÿ”„ Auto-retry** โ€” Timeout models keep getting retried, nothing is ever "given up on"
83
83
  - **๐ŸŽฎ Interactive selection** โ€” Navigate with arrow keys directly in the table, press Enter to act
84
- - **๐Ÿ”€ Startup mode menu** โ€” Choose between OpenCode and OpenClaw before the TUI launches
85
84
  - **๐Ÿ’ป OpenCode integration** โ€” Auto-detects NIM setup, sets model as default, launches OpenCode
86
85
  - **๐Ÿฆž OpenClaw integration** โ€” Sets selected model as default provider in `~/.openclaw/openclaw.json`
86
+ - **๐Ÿงฐ Public tool launchers** โ€” `Enter` can auto-configure and launch `OpenCode CLI`, `OpenCode Desktop`, `OpenClaw`, `Crush`, and `Goose`
87
87
  - **๐Ÿ“ Feature Request (J key)** โ€” Send anonymous feedback directly to the project team
88
88
  - **๐Ÿ› Bug Report (I key)** โ€” Send anonymous bug reports directly to the project team
89
89
  - **๐ŸŽจ Clean output** โ€” Zero scrollback pollution, interface stays open until Ctrl+C
@@ -150,7 +150,7 @@ bunx free-coding-models YOUR_API_KEY
150
150
  ## ๐Ÿš€ Usage
151
151
 
152
152
  ```bash
153
- # Just run it โ€” shows a startup menu to pick OpenCode or OpenClaw, prompts for API key if not set
153
+ # Just run it โ€” starts in OpenCode CLI mode, prompts for API key if not set
154
154
  free-coding-models
155
155
 
156
156
  # Explicitly target OpenCode CLI (TUI + Enter launches OpenCode CLI)
@@ -162,6 +162,10 @@ free-coding-models --opencode-desktop
162
162
  # Explicitly target OpenClaw (TUI + Enter sets model as default in OpenClaw)
163
163
  free-coding-models --openclaw
164
164
 
165
+ # Launch other supported public tools with the selected model
166
+ free-coding-models --crush
167
+ free-coding-models --goose
168
+
165
169
  # Show only top-tier models (A+, S, S+)
166
170
  free-coding-models --best
167
171
 
@@ -179,30 +183,17 @@ free-coding-models --openclaw --tier S
179
183
  free-coding-models --opencode --best
180
184
  ```
181
185
 
182
- ### Startup mode menu
183
-
184
- When you run `free-coding-models` without `--opencode` or `--openclaw`, you get an interactive startup menu:
185
-
186
- ```
187
- โšก Free Coding Models โ€” Choose your tool
188
-
189
- โฏ ๐Ÿ’ป OpenCode CLI
190
- Press Enter on a model โ†’ launch OpenCode CLI with it as default
186
+ ### Choosing the target tool
191
187
 
192
- ๐Ÿ–ฅ OpenCode Desktop
193
- Press Enter on a model โ†’ set model & open OpenCode Desktop app
188
+ Running `free-coding-models` with no launcher flag starts in **OpenCode CLI** mode.
194
189
 
195
- ๐Ÿฆž OpenClaw
196
- Press Enter on a model โ†’ set it as default in OpenClaw config
197
-
198
- โ†‘โ†“ Navigate โ€ข Enter Select โ€ข Ctrl+C Exit
199
- ```
200
-
201
- Use `โ†‘โ†“` arrows to select, `Enter` to confirm. Then the TUI launches with your chosen mode shown in the header badge.
190
+ - Press **`Z`** in the TUI to cycle the public launch targets: `OpenCode CLI` โ†’ `OpenCode Desktop` โ†’ `OpenClaw` โ†’ `Crush` โ†’ `Goose`
191
+ - Or start directly in the target mode with a CLI flag such as `--opencode-desktop`, `--openclaw`, `--crush`, or `--goose`
192
+ - The active target is always visible in the header badge before you press `Enter`
202
193
 
203
194
  **How it works:**
204
- 1. **Ping phase** โ€” All enabled models are pinged in parallel (up to 150 across 20 providers)
205
- 2. **Continuous monitoring** โ€” Models start at 2s re-pings for 60s, then fall back to 10s automatically
195
+ 1. **Ping phase** โ€” All enabled models are pinged in parallel (up to 159 across 20 providers)
196
+ 2. **Continuous monitoring** โ€” Models start at 2s re-pings for 60s, then fall back to 10s automatically, and slow to 30s after 5 minutes idle unless you force 4s mode with `W`
206
197
  3. **Real-time updates** โ€” Watch "Latest", "Avg", and "Up%" columns update live
207
198
  4. **Select anytime** โ€” Use โ†‘โ†“ arrows to navigate, press Enter on a model to act
208
199
  5. **Smart detection** โ€” Automatically detects if NVIDIA NIM is configured in OpenCode or OpenClaw
@@ -257,14 +248,18 @@ Press **`P`** to open the Settings screen at any time:
257
248
  2) Profile โ†’ API Keys โ†’ Generate
258
249
  3) Press T to test your key
259
250
 
260
- โ†‘โ†“ Navigate โ€ข Enter Edit key / Check-or-Install update โ€ข Space Toggle enabled โ€ข T Test key โ€ข U Check updates โ€ข Esc Close
251
+ โ†‘โ†“ Navigate โ€ข Enter Edit/Run โ€ข + Add key โ€ข - Remove key โ€ข Space Toggle โ€ข T Test key โ€ข S Syncโ†’OpenCode โ€ข R Restore backup โ€ข U Updates โ€ข โŒซ Delete profile โ€ข Esc Close
261
252
  ```
262
253
 
263
254
  - **โ†‘โ†“** โ€” navigate providers
264
- - **Enter** โ€” enter inline key edit mode (type your key, Enter to save, Esc to cancel)
255
+ - **Enter** โ€” edit the selected key, run maintenance actions, or load the selected profile
256
+ - **+ / -** โ€” add another key for the selected provider or remove one
265
257
  - **Space** โ€” toggle provider enabled/disabled
266
258
  - **T** โ€” fire a real test ping to verify the key works (shows โœ…/โŒ)
259
+ - **S** โ€” sync `fcm-proxy` into OpenCode when proxy mode + persistence are enabled
260
+ - **R** โ€” restore the last OpenCode backup created by sync/cleanup flows
267
261
  - **U** โ€” manually check npm for a newer version
262
+ - **Backspace** โ€” delete the selected saved profile
268
263
  - **Esc** โ€” close settings and reload models list
269
264
 
270
265
  Keys are saved to `~/.free-coding-models.json` (permissions `0600`).
@@ -364,7 +359,7 @@ TOGETHER_API_KEY=together_xxx free-coding-models
364
359
  2. Create API key (`PERPLEXITY_API_KEY`)
365
360
 
366
361
  **Alibaba Cloud (DashScope)** (8 models, Qwen3-Coder family):
367
- 1. Sign up at [dashscope.console.alibabacloud.com](https://dashscope.console.alibabacloud.com)
362
+ 1. Sign up at [modelstudio.console.alibabacloud.com](https://modelstudio.console.alibabacloud.com)
368
363
  2. Activate Model Studio (1M free tokens per model, Singapore region, 90 days)
369
364
  3. Create API key (`DASHSCOPE_API_KEY`)
370
365
 
@@ -513,6 +508,11 @@ Stability = 0.30 ร— p95_score
513
508
 
514
509
  `free-coding-models` includes a built-in reverse proxy that can group all your provider accounts into a single virtual provider.
515
510
 
511
+ Important:
512
+ - **Disabled by default** โ€” proxy mode is now opt-in from the Settings screen (`P`)
513
+ - **Direct OpenCode launch remains the default** when proxy mode is off
514
+ - **Token/request logs are only populated by proxied requests** today
515
+
516
516
  ### Why use the proxy?
517
517
  - **Unified Provider**: Instead of managing 20+ providers in your coding assistant, just use `fcm-proxy`.
518
518
  - **Automatic Rotation**: When one account hits its rate limit (429), the proxy automatically swaps to the next available account for that model.
@@ -520,18 +520,26 @@ Stability = 0.30 ร— p95_score
520
520
  - **Transparent Bridging**: Automatically handles non-standard API paths (like ZAI's `/api/coding/paas/v4/`) and converts them to standard OpenAI-compatible `/v1/` calls.
521
521
 
522
522
  ### How to use it
523
- The proxy starts automatically when you select a model in OpenCode mode if you have `fcm-proxy` configured. You can see its status (port and active account count) in the TUI footer.
523
+ 1. Open **Settings** with `P`
524
+ 2. Enable **Proxy mode (opt-in)**
525
+ 3. Optionally enable **Persist proxy in OpenCode** if you explicitly want `fcm-proxy` written to `~/.config/opencode/opencode.json`
526
+ 4. Optionally set **Preferred proxy port** (`0` = auto)
527
+ 5. Use `S` in Settings to sync the proxy catalog into OpenCode only when you actually want that persistent config
528
+
529
+ Cleanup:
530
+ - Use the Settings action **Clean OpenCode proxy config**
531
+ - Or run `free-coding-models --clean-proxy`
524
532
 
525
533
  ---
526
534
 
527
- ## ๐Ÿ“œ Log Viewer
535
+ ## ๐Ÿ“œ Request Log Overlay
528
536
 
529
- Press **`X`** at any time to open the dedicated Log Viewer overlay.
537
+ Press **`X`** at any time to open the dedicated request-log overlay.
530
538
 
531
- - **Real-time Activity**: See every ping, rotation, and proxy request as it happens.
532
- - **Error Diagnostics**: View detailed error messages from providers when a ping fails.
533
- - **Quota Tracking**: Monitor how the tool discovers and updates your remaining quota.
534
- - **Auto-Pruning**: The log history is automatically managed to stay concise and relevant.
539
+ - **Proxy-only accounting**: Entries are written when requests flow through the multi-account proxy.
540
+ - **Exact token totals**: The overlay aggregates prompt+completion usage per proxied request.
541
+ - **Per-request visibility**: You can inspect provider, model, status, token count, and latency for recent requests.
542
+ - **Startup table reuse**: The `Used` column in the main table is derived from the same request log file.
535
543
 
536
544
  Use **โ†‘โ†“** to scroll and **Esc** or **X** to return to the main table.
537
545
 
@@ -541,7 +549,7 @@ Use **โ†‘โ†“** to scroll and **Esc** or **X** to return to the main table.
541
549
 
542
550
  **The easiest way** โ€” let `free-coding-models` do everything:
543
551
 
544
- 1. **Run**: `free-coding-models --opencode` (or choose OpenCode from the startup menu)
552
+ 1. **Run**: `free-coding-models --opencode` (or launch with no flag to use the default OpenCode CLI mode)
545
553
  2. **Wait** for models to be pinged (green โœ… status)
546
554
  3. **Navigate** with โ†‘โ†“ arrows to your preferred model
547
555
  4. **Press Enter** โ€” tool automatically:
@@ -624,7 +632,7 @@ OpenClaw is an autonomous AI agent daemon. `free-coding-models` can configure it
624
632
  free-coding-models --openclaw
625
633
  ```
626
634
 
627
- Or run without flags and choose **OpenClaw** from the startup menu.
635
+ Or press **`Z`** in the TUI until the header shows **OpenClaw**, then press **Enter** on a model.
628
636
 
629
637
  1. **Wait** for models to be pinged
630
638
  2. **Navigate** with โ†‘โ†“ arrows to your preferred model
@@ -729,6 +737,7 @@ This script:
729
737
  โ”‚ 2. Ping ALL models in parallel โ”‚
730
738
  โ”‚ 3. Display real-time table with Latest/Avg/Stability/Up% โ”‚
731
739
  โ”‚ 4. Re-ping ALL models at 2s on startup, then 10s steady-state โ”‚
740
+ โ”‚ and 30s after 5m idle unless forced back to 4s with W โ”‚
732
741
  โ”‚ 5. Update rolling averages + stability scores per model โ”‚
733
742
  โ”‚ 6. User can navigate with โ†‘โ†“ and select with Enter โ”‚
734
743
  โ”‚ 7. On Enter (OpenCode): set model, launch OpenCode โ”‚
@@ -816,29 +825,32 @@ This script:
816
825
 
817
826
  | Flag | Description |
818
827
  |------|-------------|
819
- | *(none)* | Show startup menu to choose OpenCode or OpenClaw |
828
+ | *(none)* | Start in OpenCode CLI mode |
820
829
  | `--opencode` | OpenCode CLI mode โ€” Enter launches OpenCode CLI with selected model |
821
- | `--opencode-desktop` | OpenCode Desktop mode โ€” Enter sets model & opens OpenCode Desktop app |
822
- | `--openclaw` | OpenClaw mode โ€” Enter sets selected model as default in OpenClaw |
823
- | `--best` | Show only top-tier models (A+, S, S+) |
824
- | `--fiable` | Analyze 10 seconds, output the most reliable model as `provider/model_id` |
825
- | `--tier S` | Show only S+ and S tier models |
830
+ | `--opencode-desktop` | OpenCode Desktop mode โ€” Enter sets model and opens OpenCode Desktop |
831
+ | `--openclaw` | OpenClaw mode โ€” Enter sets selected model as default in OpenClaw |
832
+ | `--crush` | Crush mode โ€” Enter writes `crush.json` and launches Crush |
833
+ | `--goose` | Goose mode โ€” Enter launches Goose with env-based provider config |
834
+ | `--best` | Show only top-tier models (A+, S, S+) |
835
+ | `--fiable` | Analyze 10 seconds, output the most reliable model as `provider/model_id` |
836
+ | `--tier S` | Show only S+ and S tier models |
826
837
  | `--tier A` | Show only A+, A, A- tier models |
827
838
  | `--tier B` | Show only B+, B tier models |
828
839
  | `--tier C` | Show only C tier models |
829
840
  | `--profile <name>` | Load a saved config profile on startup |
830
841
  | `--recommend` | Auto-open Smart Recommend overlay on start |
842
+ | `--clean-proxy` | Remove persisted `fcm-proxy` config from OpenCode |
831
843
 
832
844
  **Keyboard shortcuts (main TUI):**
833
845
  - **โ†‘โ†“** โ€” Navigate models
834
- - **Enter** โ€” Select model (launches OpenCode or sets OpenClaw default, depending on mode)
846
+ - **Enter** โ€” Select model and launch the current target tool from the header badge
835
847
  - **R/Y/S/C/M/O/L/A/H/V/B/U/G** โ€” Sort by Rank/Tier/SWE/Ctx/Model/Provider/Latest/Avg/Health/Verdict/Stability/Up%/Usage
836
848
  - **F** โ€” Toggle favorite on selected model (โญ in Model column, pinned at top)
837
849
  - **T** โ€” Cycle tier filter (All โ†’ S+ โ†’ S โ†’ A+ โ†’ A โ†’ A- โ†’ B+ โ†’ B โ†’ C โ†’ All)
838
850
  - **D** โ€” Cycle provider filter (All โ†’ NIM โ†’ Groq โ†’ ...)
839
851
  - **E** โ€” Toggle configured-only mode (on by default, persisted across sessions and profiles)
840
- - **Z** โ€” Cycle mode (OpenCode CLI โ†’ OpenCode Desktop โ†’ OpenClaw)
841
- - **X** โ€” **Toggle Token Logs** (view recent request/token usage logs)
852
+ - **Z** โ€” Cycle target tool (OpenCode CLI โ†’ OpenCode Desktop โ†’ OpenClaw โ†’ Crush โ†’ Goose)
853
+ - **X** โ€” Toggle request logs (recent proxied request/token usage logs)
842
854
  - **P** โ€” Open Settings (manage API keys, toggles, updates, profiles)
843
855
  - **Shift+P** โ€” Cycle through saved profiles (switches live TUI settings)
844
856
  - **Shift+S** โ€” Save current TUI settings as a named profile (inline prompt)
@@ -870,6 +882,7 @@ Profiles let you save and restore different TUI configurations โ€” useful if you
870
882
  - Sort column and direction
871
883
  - Tier filter
872
884
  - Ping mode
885
+ - Configured-only filter
873
886
  - API keys
874
887
 
875
888
  **Saving a profile:**
@@ -15,8 +15,8 @@
15
15
  * - Rolling averages calculated from ALL successful pings since start
16
16
  * - Best-per-tier highlighting with medals (๐Ÿฅ‡๐Ÿฅˆ๐Ÿฅ‰)
17
17
  * - Interactive navigation with arrow keys directly in the table
18
- * - Instant OpenCode OR OpenClaw action on Enter key press
19
- * - Startup mode menu (OpenCode CLI vs OpenCode Desktop vs OpenClaw) when no flag is given
18
+ * - Instant OpenCode / OpenClaw / external-tool action on Enter key press
19
+ * - Direct mode flags plus an in-app Z-cycle for the public launcher set
20
20
  * - Automatic config detection and model setup for both tools
21
21
  * - JSON config stored in ~/.free-coding-models.json (auto-migrates from old plain-text)
22
22
  * - Multi-provider support via sources.js (NIM/Groq/Cerebras/OpenRouter/Hugging Face/Replicate/DeepInfra/... โ€” extensible)
@@ -34,7 +34,6 @@
34
34
  * - `sendUsageTelemetry`: Fire-and-forget anonymous app-start event
35
35
  * - `ensureFavoritesConfig` / `toggleFavoriteModel`: Persist and toggle pinned favorites
36
36
  * - `promptApiKey`: Interactive wizard for first-time multi-provider API key setup
37
- * - `promptModeSelection`: Startup menu to choose OpenCode vs OpenClaw
38
37
  * - `buildPingRequest` / `ping`: Build provider-specific probe requests and measure latency
39
38
  * - `renderTable`: Generate ASCII table with colored latency indicators and status emojis
40
39
  * - `getAvg`: Calculate average latency from all successful pings
@@ -66,14 +65,15 @@
66
65
  * - OpenCode config: ~/.config/opencode/opencode.json
67
66
  * - OpenClaw config: ~/.openclaw/openclaw.json
68
67
  * - Ping timeout: 15s per attempt
69
- * - Ping interval: 60 seconds (continuous monitoring mode)
68
+ * - Ping cadence: 2s startup burst for 60s, 10s steady state, 30s after 5m idle, forced 4s via `W`
70
69
  * - Animation: 12 FPS with braille spinners
71
70
  *
72
71
  * ๐Ÿš€ CLI flags:
73
- * - (no flag): Show startup menu โ†’ choose OpenCode or OpenClaw
72
+ * - (no flag): Start in OpenCode CLI mode
74
73
  * - --opencode: OpenCode CLI mode (launch CLI with selected model)
75
74
  * - --opencode-desktop: OpenCode Desktop mode (set model & open Desktop app)
76
75
  * - --openclaw: OpenClaw mode (set selected model as default in OpenClaw)
76
+ * - --crush / --goose: launch the currently selected model in the supported external CLI
77
77
  * - --best: Show only top-tier models (A+, S, S+)
78
78
  * - --fiable: Analyze 10s and output the most reliable model
79
79
  * - --no-telemetry: Disable anonymous usage analytics for this run
@@ -92,10 +92,10 @@ import { homedir } from 'os'
92
92
  import { join, dirname } from 'path'
93
93
  import { MODELS, sources } from '../sources.js'
94
94
  import { getAvg, getVerdict, getUptime, getP95, getJitter, getStabilityScore, sortResults, filterByTier, findBestModel, parseArgs, TIER_ORDER, VERDICT_ORDER, TIER_LETTER_MAP, scoreModelForTask, getTopRecommendations, TASK_TYPES, PRIORITY_TYPES, CONTEXT_BUDGETS, formatCtxWindow, labelFromId, getProxyStatusInfo } from '../src/utils.js'
95
- import { loadConfig, saveConfig, getApiKey, resolveApiKeys, addApiKey, removeApiKey, isProviderEnabled, saveAsProfile, loadProfile, listProfiles, deleteProfile, getActiveProfileName, setActiveProfile, _emptyProfileSettings } from '../src/config.js'
95
+ import { loadConfig, saveConfig, getApiKey, getProxySettings, resolveApiKeys, addApiKey, removeApiKey, isProviderEnabled, saveAsProfile, loadProfile, listProfiles, deleteProfile, getActiveProfileName, setActiveProfile, _emptyProfileSettings } from '../src/config.js'
96
96
  import { buildMergedModels } from '../src/model-merger.js'
97
97
  import { ProxyServer } from '../src/proxy-server.js'
98
- import { loadOpenCodeConfig, saveOpenCodeConfig, syncToOpenCode, restoreOpenCodeBackup } from '../src/opencode-sync.js'
98
+ import { loadOpenCodeConfig, saveOpenCodeConfig, syncToOpenCode, restoreOpenCodeBackup, cleanupOpenCodeProxyConfig } from '../src/opencode-sync.js'
99
99
  import { usageForRow as _usageForRow } from '../src/usage-reader.js'
100
100
  import { loadRecentLogs } from '../src/log-reader.js'
101
101
  import { buildProviderModelTokenKey, loadTokenUsageByProviderModel } from '../src/token-usage-reader.js'
@@ -112,10 +112,12 @@ import { checkForUpdateDetailed, checkForUpdate, runUpdate, promptUpdateNotifica
112
112
  import { promptApiKey } from '../src/setup.js'
113
113
  import { stripAnsi, maskApiKey, displayWidth, padEndDisplay, tintOverlayLines, keepOverlayTargetVisible, sliceOverlayLines, calculateViewport, sortResultsWithPinnedFavorites, renderProxyStatusLine, adjustScrollOffset } from '../src/render-helpers.js'
114
114
  import { renderTable } from '../src/render-table.js'
115
- import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop, startProxyAndLaunch, autoStartProxyIfSynced, ensureProxyRunning, buildProxyTopologyFromConfig } from '../src/opencode.js'
115
+ import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop, startProxyAndLaunch, autoStartProxyIfSynced, ensureProxyRunning, buildProxyTopologyFromConfig, isProxyEnabledForConfig } from '../src/opencode.js'
116
116
  import { startOpenClaw } from '../src/openclaw.js'
117
117
  import { createOverlayRenderers } from '../src/overlays.js'
118
118
  import { createKeyHandler } from '../src/key-handler.js'
119
+ import { getToolModeOrder } from '../src/tool-metadata.js'
120
+ import { startExternalTool } from '../src/tool-launchers.js'
119
121
 
120
122
  // ๐Ÿ“– mergedModels: cross-provider grouped model list (one entry per label, N providers each)
121
123
  // ๐Ÿ“– mergedModelByLabel: fast lookup map from display label โ†’ merged model entry
@@ -176,6 +178,16 @@ async function main() {
176
178
  ensureTelemetryConfig(config)
177
179
  ensureFavoritesConfig(config)
178
180
 
181
+ if (cliArgs.cleanProxyMode) {
182
+ const cleaned = cleanupOpenCodeProxyConfig()
183
+ console.log()
184
+ console.log(chalk.green(' โœ… OpenCode proxy cleanup complete'))
185
+ console.log(chalk.dim(` Config: ${cleaned.path}`))
186
+ console.log(chalk.dim(` Removed provider: ${cleaned.removedProvider ? 'yes' : 'no'} โ€ข Removed default model: ${cleaned.removedModel ? 'yes' : 'no'}`))
187
+ console.log()
188
+ process.exit(0)
189
+ }
190
+
179
191
  // ๐Ÿ“– If --profile <name> was passed, load that profile into the live config
180
192
  let startupProfileSettings = null
181
193
  if (cliArgs.profileName) {
@@ -204,11 +216,28 @@ async function main() {
204
216
  // ๐Ÿ“– Backward-compat: keep apiKey var for startOpenClaw() which still needs it
205
217
  let apiKey = getApiKey(config, 'nvidia')
206
218
 
207
- // ๐Ÿ“– Default mode: OpenCode CLI
219
+ // ๐Ÿ“– Default mode: OpenCode CLI.
220
+ // ๐Ÿ“– Additional external tools can now be selected via dedicated flags.
208
221
  let mode = 'opencode'
209
- if (cliArgs.openClawMode) mode = 'openclaw'
210
- else if (cliArgs.openCodeDesktopMode) mode = 'opencode-desktop'
211
- else if (cliArgs.openCodeMode) mode = 'opencode'
222
+ const requestedMode = getToolModeOrder().find((toolMode) => {
223
+ const flagByMode = {
224
+ opencode: cliArgs.openCodeMode,
225
+ 'opencode-desktop': cliArgs.openCodeDesktopMode,
226
+ openclaw: cliArgs.openClawMode,
227
+ aider: cliArgs.aiderMode,
228
+ crush: cliArgs.crushMode,
229
+ goose: cliArgs.gooseMode,
230
+ 'claude-code': cliArgs.claudeCodeMode,
231
+ codex: cliArgs.codexMode,
232
+ gemini: cliArgs.geminiMode,
233
+ qwen: cliArgs.qwenMode,
234
+ openhands: cliArgs.openHandsMode,
235
+ amp: cliArgs.ampMode,
236
+ pi: cliArgs.piMode,
237
+ }
238
+ return flagByMode[toolMode] === true
239
+ })
240
+ if (requestedMode) mode = requestedMode
212
241
 
213
242
  // ๐Ÿ“– Track app opening early so fast exits are still counted.
214
243
  // ๐Ÿ“– Must run before update checks because npm registry lookups can add startup delay.
@@ -353,6 +382,8 @@ async function main() {
353
382
  scrollOffset: 0, // ๐Ÿ“– First visible model index in viewport
354
383
  terminalRows: process.stdout.rows || 24, // ๐Ÿ“– Current terminal height
355
384
  terminalCols: process.stdout.columns || 80, // ๐Ÿ“– Current terminal width
385
+ widthWarningStartedAt: (process.stdout.columns || 80) < 166 ? now : null, // ๐Ÿ“– Start the narrow-terminal countdown immediately when booting in a small viewport.
386
+ widthWarningDismissed: false, // ๐Ÿ“– Esc hides the narrow-terminal warning early for the current narrow-width session.
356
387
  // ๐Ÿ“– Settings screen state (P key opens it)
357
388
  settingsOpen: false, // ๐Ÿ“– Whether settings overlay is active
358
389
  settingsCursor: 0, // ๐Ÿ“– Which provider row is selected in settings
@@ -364,6 +395,8 @@ async function main() {
364
395
  settingsUpdateState: 'idle', // ๐Ÿ“– 'idle'|'checking'|'available'|'up-to-date'|'error'|'installing'
365
396
  settingsUpdateLatestVersion: null, // ๐Ÿ“– Latest npm version discovered from manual check
366
397
  settingsUpdateError: null, // ๐Ÿ“– Last update-check error message for maintenance row
398
+ settingsProxyPortEditMode: false, // ๐Ÿ“– Whether Settings is editing the preferred proxy port field.
399
+ settingsProxyPortBuffer: '', // ๐Ÿ“– Inline input buffer for the preferred proxy port (0 = auto).
367
400
  config, // ๐Ÿ“– Live reference to the config object (updated on save)
368
401
  visibleSorted: [], // ๐Ÿ“– Cached visible+sorted models โ€” shared between render loop and key handlers
369
402
  helpVisible: false, // ๐Ÿ“– Whether the help overlay (K key) is active
@@ -410,8 +443,20 @@ async function main() {
410
443
 
411
444
  // ๐Ÿ“– Re-clamp viewport on terminal resize
412
445
  process.stdout.on('resize', () => {
446
+ const prevCols = state.terminalCols
413
447
  state.terminalRows = process.stdout.rows || 24
414
448
  state.terminalCols = process.stdout.columns || 80
449
+ if (state.terminalCols < 166) {
450
+ if (prevCols >= 166 || state.widthWarningDismissed) {
451
+ state.widthWarningStartedAt = Date.now()
452
+ state.widthWarningDismissed = false
453
+ } else if (!state.widthWarningStartedAt) {
454
+ state.widthWarningStartedAt = Date.now()
455
+ }
456
+ } else {
457
+ state.widthWarningStartedAt = null
458
+ state.widthWarningDismissed = false
459
+ }
415
460
  adjustScrollOffset(state)
416
461
  })
417
462
 
@@ -524,6 +569,7 @@ async function main() {
524
569
  PROVIDER_METADATA,
525
570
  LOCAL_VERSION,
526
571
  getApiKey,
572
+ getProxySettings,
527
573
  resolveApiKeys,
528
574
  isProviderEnabled,
529
575
  listProfiles,
@@ -557,6 +603,7 @@ async function main() {
557
603
  MODELS,
558
604
  sources,
559
605
  getApiKey,
606
+ getProxySettings,
560
607
  resolveApiKeys,
561
608
  addApiKey,
562
609
  removeApiKey,
@@ -578,6 +625,7 @@ async function main() {
578
625
  ENV_VAR_NAMES,
579
626
  ensureProxyRunning,
580
627
  syncToOpenCode,
628
+ cleanupOpenCodeProxyConfig,
581
629
  restoreOpenCodeBackup,
582
630
  checkForUpdateDetailed,
583
631
  runUpdate,
@@ -585,7 +633,10 @@ async function main() {
585
633
  startOpenCodeDesktop,
586
634
  startOpenCode,
587
635
  startProxyAndLaunch,
636
+ startExternalTool,
588
637
  buildProxyTopologyFromConfig,
638
+ isProxyEnabledForConfig,
639
+ getToolModeOrder,
589
640
  startRecommendAnalysis: overlays.startRecommendAnalysis,
590
641
  stopRecommendAnalysis: overlays.stopRecommendAnalysis,
591
642
  sendFeatureRequest,
@@ -652,7 +703,7 @@ async function main() {
652
703
  ? overlays.renderHelp()
653
704
  : state.logVisible
654
705
  ? overlays.renderLog()
655
- : 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, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels)
706
+ : 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, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed)
656
707
  process.stdout.write(ALT_HOME + content)
657
708
  }, Math.round(1000 / FPS))
658
709
 
@@ -660,7 +711,7 @@ async function main() {
660
711
  const initialVisible = state.results.filter(r => !r.hidden)
661
712
  state.visibleSorted = sortResultsWithPinnedFavorites(initialVisible, state.sortColumn, state.sortDirection)
662
713
 
663
- 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, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels))
714
+ 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, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed))
664
715
 
665
716
  // ๐Ÿ“– If --recommend was passed, auto-open the Smart Recommend overlay on start
666
717
  if (cliArgs.recommendMode) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.1.87",
3
+ "version": "0.2.0",
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/config.js CHANGED
@@ -71,7 +71,7 @@
71
71
  * - apiKeys: API keys per provider (can differ between work/personal setups)
72
72
  * - providers: enabled/disabled state per provider
73
73
  * - favorites: list of pinned favorite models
74
- * - settings: extra TUI preferences (tierFilter, sortColumn, sortAsc, pingInterval, hideUnconfiguredModels)
74
+ * - settings: extra TUI preferences (tierFilter, sortColumn, sortAsc, pingInterval, hideUnconfiguredModels, proxy)
75
75
  *
76
76
  * ๐Ÿ“– When a profile is loaded via --profile <name> or Shift+P, the main config's
77
77
  * apiKeys/providers/favorites are replaced with the profile's values. The profile
@@ -96,11 +96,12 @@
96
96
  * โ†’ getActiveProfileName(config) โ€” Get the currently active profile name (or null)
97
97
  * โ†’ setActiveProfile(config, name) โ€” Set which profile is active (null to clear)
98
98
  * โ†’ _emptyProfileSettings() โ€” Default TUI settings for a profile
99
+ * โ†’ getProxySettings(config) โ€” Return normalized proxy settings from config
99
100
  *
100
101
  * @exports loadConfig, saveConfig, getApiKey, isProviderEnabled
101
102
  * @exports addApiKey, removeApiKey, listApiKeys โ€” multi-key management helpers
102
103
  * @exports saveAsProfile, loadProfile, listProfiles, deleteProfile
103
- * @exports getActiveProfileName, setActiveProfile
104
+ * @exports getActiveProfileName, setActiveProfile, getProxySettings
104
105
  * @exports CONFIG_PATH โ€” path to the JSON config file
105
106
  *
106
107
  * @see bin/free-coding-models.js โ€” main CLI that uses these functions
@@ -166,6 +167,7 @@ export function loadConfig() {
166
167
  if (!parsed.providers) parsed.providers = {}
167
168
  if (!parsed.settings || typeof parsed.settings !== 'object') parsed.settings = {}
168
169
  if (typeof parsed.settings.hideUnconfiguredModels !== 'boolean') parsed.settings.hideUnconfiguredModels = true
170
+ parsed.settings.proxy = normalizeProxySettings(parsed.settings.proxy)
169
171
  // ๐Ÿ“– Favorites: list of "providerKey/modelId" pinned rows.
170
172
  if (!Array.isArray(parsed.favorites)) parsed.favorites = []
171
173
  parsed.favorites = parsed.favorites.filter((fav) => typeof fav === 'string' && fav.trim().length > 0)
@@ -177,7 +179,9 @@ export function loadConfig() {
177
179
  if (!parsed.profiles || typeof parsed.profiles !== 'object') parsed.profiles = {}
178
180
  for (const profile of Object.values(parsed.profiles)) {
179
181
  if (!profile || typeof profile !== 'object') continue
180
- profile.settings = profile.settings ? { ..._emptyProfileSettings(), ...profile.settings } : _emptyProfileSettings()
182
+ profile.settings = profile.settings
183
+ ? { ..._emptyProfileSettings(), ...profile.settings, proxy: normalizeProxySettings(profile.settings.proxy) }
184
+ : _emptyProfileSettings()
181
185
  }
182
186
  if (parsed.activeProfile && typeof parsed.activeProfile !== 'string') parsed.activeProfile = null
183
187
  return parsed
@@ -400,9 +404,40 @@ export function _emptyProfileSettings() {
400
404
  sortAsc: true, // ๐Ÿ“– true = ascending (fastest first for latency)
401
405
  pingInterval: 10000, // ๐Ÿ“– default ms between pings in the steady "normal" mode
402
406
  hideUnconfiguredModels: true, // ๐Ÿ“– true = default to providers that are actually configured
407
+ proxy: normalizeProxySettings(),
403
408
  }
404
409
  }
405
410
 
411
+ /**
412
+ * ๐Ÿ“– normalizeProxySettings: keep proxy-related preferences stable across old configs,
413
+ * ๐Ÿ“– new installs, and profile switches. Proxy is opt-in by default.
414
+ *
415
+ * @param {object|undefined|null} proxy
416
+ * @returns {{ enabled: boolean, syncToOpenCode: boolean, preferredPort: number }}
417
+ */
418
+ export function normalizeProxySettings(proxy = null) {
419
+ const preferredPort = Number.isInteger(proxy?.preferredPort) && proxy.preferredPort >= 0 && proxy.preferredPort <= 65535
420
+ ? proxy.preferredPort
421
+ : 0
422
+
423
+ return {
424
+ enabled: proxy?.enabled === true,
425
+ syncToOpenCode: proxy?.syncToOpenCode === true,
426
+ preferredPort,
427
+ }
428
+ }
429
+
430
+ /**
431
+ * ๐Ÿ“– getProxySettings: return normalized proxy settings from the live config.
432
+ * ๐Ÿ“– This centralizes the opt-in default so launchers do not guess.
433
+ *
434
+ * @param {object} config
435
+ * @returns {{ enabled: boolean, syncToOpenCode: boolean, preferredPort: number }}
436
+ */
437
+ export function getProxySettings(config) {
438
+ return normalizeProxySettings(config?.settings?.proxy)
439
+ }
440
+
406
441
  /**
407
442
  * ๐Ÿ“– saveAsProfile: Snapshot the current config state into a named profile.
408
443
  *
@@ -447,14 +482,16 @@ export function saveAsProfile(config, name, settings = null) {
447
482
  export function loadProfile(config, name) {
448
483
  const profile = config?.profiles?.[name]
449
484
  if (!profile) return null
485
+ const nextSettings = profile.settings ? { ..._emptyProfileSettings(), ...profile.settings, proxy: normalizeProxySettings(profile.settings.proxy) } : _emptyProfileSettings()
450
486
 
451
487
  // ๐Ÿ“– Deep-copy the profile data into the live config (don't share references)
452
488
  config.apiKeys = JSON.parse(JSON.stringify(profile.apiKeys || {}))
453
489
  config.providers = JSON.parse(JSON.stringify(profile.providers || {}))
454
490
  config.favorites = [...(profile.favorites || [])]
491
+ config.settings = nextSettings
455
492
  config.activeProfile = name
456
493
 
457
- return profile.settings ? { ..._emptyProfileSettings(), ...profile.settings } : _emptyProfileSettings()
494
+ return nextSettings
458
495
  }
459
496
 
460
497
  /**
@@ -515,6 +552,7 @@ function _emptyConfig() {
515
552
  // ๐Ÿ“– Global TUI preferences that should persist even without a named profile.
516
553
  settings: {
517
554
  hideUnconfiguredModels: true,
555
+ proxy: normalizeProxySettings(),
518
556
  },
519
557
  // ๐Ÿ“– Pinned favorites rendered at top of the table ("providerKey/modelId").
520
558
  favorites: [],