free-coding-models 0.3.65 → 0.3.67

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,10 +1,45 @@
1
- ## [0.3.65] - 2026-05-06
1
+ ## [0.3.67] - 2026-05-17
2
2
 
3
- ### Fixed
3
+ ### Added
4
4
 
5
- - **Live Usable only filtering** `Usable only` now refreshes continuously while pings run, so models enter the table as soon as they become usable and leave the table as soon as they stop being usable. The filter now reflects the current `UP` state and usable verdicts instead of staying stuck on the state from when the filter was toggled.
6
- - **Sticky favorites no longer bypass Usable only health** Favorites can still stay visible across tier, provider, and text filters when sticky favorites mode is enabled, but `Usable only` now takes precedence. A favorite that times out, goes down, hits auth errors, or otherwise stops being usable is removed from the view like any other model.
5
+ - **🐳 Docker Packaging**: First-class Docker support so you can run FCM without installing Node.js. The official image is published to `ghcr.io/vava-nessa/free-coding-models` on every release and tag push.
6
+ - Multi-arch friendly `Dockerfile` based on `node:20-alpine`, running as a non-root `fcm` user.
7
+ - `docker-entrypoint.sh` auto-generates `~/.free-coding-models.json` from any `*_API_KEY` / `*_API_TOKEN` env vars you pass to the container — no manual config step.
8
+ - `docker-compose.yml` template wired up for every supported provider.
9
+ - GitHub Actions workflow (`.github/workflows/docker.yml`) handles build + publish to GHCR on `release: published`, `push: v*.*.*` tags, and `workflow_dispatch` (with a `test_mode` dry-run input).
10
+ - Trivy vulnerability scan blocks releases that introduce any CRITICAL/HIGH CVEs.
11
+ - Quick start: `docker run -p 19280:19280 -e OPENROUTER_API_KEY=... ghcr.io/vava-nessa/free-coding-models:latest`.
12
+ - **🌐 Combined Daemon + Web Dashboard**: The router daemon now serves the web dashboard from the same port — no more juggling two processes. New REST surface area baked into the daemon:
13
+ - `GET /api/models` — full model catalog with latency stats, status, p95, jitter, stability, verdict, uptime, and `inRouterSet` flag.
14
+ - `GET /api/config` — provider catalog with masked API keys (`••••••••XXXX`) and enabled state.
15
+ - `GET /api/events` — SSE stream the dashboard subscribes to for live updates.
16
+ - `GET /api/key/<provider>` — reveal the raw API key for a configured provider (same-origin only).
17
+ - `POST /api/settings` — save API keys and per-provider enabled flags from the dashboard, then trigger a probe burst.
18
+ - When you add a new provider's API key from the dashboard, FCM now mirrors `--sync-set` behavior and automatically adds that provider's best-tier model to your active router set.
7
19
 
8
20
  ### Changed
9
21
 
10
- - **Safer cursor behavior during live filtering** When live filtering removes the selected row, the TUI now clamps the cursor and scroll offset to the remaining visible rows so selection and launch behavior keep matching what is on screen.
22
+ - **`--web` flag removed**: replaced by `--daemon`, which now serves both the OpenAI-compatible router API and the dashboard on the same port. Existing tooling using `--web` should switch to `--daemon`.
23
+ - **Preserve user-created router sets on daemon start**: previously the daemon rebuilt the active set from favorites or defaults on every restart, silently overwriting sets created with `--sync-set`. Named sets are now preserved.
24
+ - **Faster config reload**: `CONFIG_RELOAD_INTERVAL_MS` shortened from 60s → 10s so dashboard-driven changes (toggling providers, adding keys) propagate quickly.
25
+ - **Contributors**: welcome [@stgreenb](https://github.com/stgreenb) 🎉 — author of the daemon-web merge and Docker packaging work.
26
+
27
+ ### Fixed
28
+
29
+ - **🔒 Path traversal in dashboard static file serving (security)**: requests like `GET /../../etc/passwd` could escape `web/dist/` and read arbitrary files reachable by the daemon user. The mitigation (`127.0.0.1` bind) was bypassed inside Docker where the daemon binds `0.0.0.0`. All static paths are now resolved against `WEB_DIST_DIR` and rejected with 403 when they escape.
30
+ - **🔒 Cross-site write / key exfiltration on dashboard endpoints (security)**: `POST /api/settings` (writes API keys) and `GET /api/key/<provider>` (reveals raw keys) previously accepted any request, including those triggered by malicious tabs visiting a page that fetched `http://localhost:19280/...`. Both now enforce a same-origin / loopback `Origin` header check. Header-less CLI callers (curl, native apps) keep working.
31
+ - **🔒 Config file permissions tightened in Docker**: the entrypoint previously created `~/.free-coding-models.json` with mode `0666`. Tightened to `0600` since it stores plaintext API keys.
32
+ - **🐛 `/api/key/:provider` route never matched**: the handler compared `url.pathname === '/api/key/:provider'` literally, so the endpoint was unreachable. Now correctly matches via `startsWith` and 404s unknown providers.
33
+ - **🐛 "Excellent" verdict for fully-down models**: when a model had probe history but every ping failed, `avg` collapsed to `0` and the dashboard showed "Excellent". Verdict now correctly returns `—` whenever no usable latency sample exists.
34
+ - **🐛 `--daemon-bg` mode signalled "down" for everything in dashboard**: the type-mismatch comparison (`===` between string code and number) made every model render as `down`. Codes are now compared as strings.
35
+ - **⚙️ Docker GitHub Actions workflow YAML repaired**: the top-level `env:` block and the GHCR login step were incorrectly indented and would have prevented GitHub from parsing the workflow at all.
36
+ - **⚙️ Docker container lifecycle**: the container no longer outlives a crashed daemon. The entrypoint now runs the daemon in the foreground (`--daemon` instead of `--daemon-bg`) so Docker's restart policy can recover from crashes instead of waiting for the healthcheck to time out.
37
+
38
+ ### Internal
39
+
40
+ - Removed a dead `createReadStream` import in `router-daemon.js`.
41
+ - Hoisted `routerConfig()` and `getSet()` lookups out of the per-model loop in `getWebModelsPayload()` — saves ~200 redundant runtime calls per dashboard refresh — and uses a `Set` index for in-set membership checks.
42
+ - Renamed a local `window` variable to `probeWindow` to avoid shadowing globals.
43
+ - Removed a nested `router` shadow in `/api/settings`.
44
+ - Added `X-Content-Type-Options: nosniff` header on all static dashboard responses.
45
+ - Added 6 new tests covering path-traversal blocking, cross-origin write rejection, same-origin write success, CLI key fetch, and unknown-provider 404. Total: 371 tests, all green.
package/README.md CHANGED
@@ -1,36 +1,32 @@
1
- <table>
2
- <tr>
3
- <td style="vertical-align: middle; width: 200px;">
4
- <img src="logo.webp" alt="free-coding-models logo" width="128"><br><br>
5
- <img src="https://img.shields.io/npm/v/free-coding-models?color=3d6b00&label=npm&logo=npm" alt="npm version" width="200"><br>
6
- <img src="https://img.shields.io/node/v/free-coding-models?color=3d6b00&logo=node.js" alt="node version" width="200"><br>
7
- <img src="https://img.shields.io/npm/l/free-coding-models?color=3d6b00" alt="license" width="200"><br>
8
- <img src="https://img.shields.io/badge/models-170+-3d6b00?logo=nvidia" alt="models count" width="200"><br>
9
- <img src="https://img.shields.io/badge/providers-16-1a56db" alt="providers count" width="200">
10
- </td>
11
- <td style="vertical-align: middle;">
12
- <h1 style="margin-top: 0;">free-coding-models</h1>
13
- <strong>Find the fastest free coding model in seconds</strong><br>
14
- Track ~170 models across ~15 trusted free or free-limited AI providers in real time<br><br>
15
- <strong>Install Free API endpoints to your favorite AI coding tools:</strong><br>
16
- OpenCode CLI / Desktop / WebUI, OpenClaw, Crush, Goose, Aider, Kilo CLI, Qwen Code, OpenHands, Amp, Hermes, Continue, Cline, Xcode, Pi, Rovo, Gemini and more...<br><br>
17
- <strong>Use Kimi K2, DeepSeek V3, GPT-OSS, Qwen3, MiniMax M2, GLM, Llama 4, Gemma 4, Devstral and more — for free</strong>
18
- </td>
19
- </tr>
20
- </table>
21
-
1
+ <p align="center">
2
+ <img src="logo.webp" alt="free-coding-models logo" width="328">
3
+ </p>
22
4
 
5
+ <h1 align="center">free-coding-models</h1>
23
6
 
7
+ <p align="center">
8
+ <strong>Find the fastest free coding model in seconds</strong><br>
9
+ Track ~170 models across ~15 trusted free or free-limited AI providers in real time<br><br>
10
+ <strong>Install Free API endpoints to your favorite AI coding tools:</strong><br>
11
+ OpenCode CLI / Desktop / WebUI, OpenClaw, Crush, Goose, Aider, Kilo CLI, Qwen Code, OpenHands, Amp, Hermes, Continue, Cline, Xcode, Pi, Rovo, Gemini and more...<br><br>
12
+ <strong>Use Kimi K2, DeepSeek V3, GPT-OSS, Qwen3, MiniMax M2, GLM, Llama 4, Gemma 4, Devstral and more — for free</strong>
13
+ </p>
24
14
 
25
15
  <p align="center">
16
+ <img src="https://img.shields.io/npm/v/free-coding-models?color=3d6b00&label=npm&logo=npm" alt="npm version" width="200"><br>
17
+ <img src="https://img.shields.io/node/v/free-coding-models?color=3d6b00&logo=node.js" alt="node version" width="200"><br>
18
+ <img src="https://img.shields.io/npm/l/free-coding-models?color=3d6b00" alt="license" width="200"><br>
19
+ <img src="https://img.shields.io/badge/models-170+-3d6b00?logo=nvidia" alt="models count" width="200"><br>
20
+ <img src="https://img.shields.io/badge/providers-16-1a56db" alt="providers count" width="200">
21
+ </p>
26
22
 
27
23
  ```bash
28
24
  npm install -g free-coding-models
29
25
  free-coding-models
30
26
  ```
31
27
 
32
- create a free account on one of the [providers](#-list-of-free-ai-providers)
33
-
28
+ <p align="center">
29
+ create a free account on one of the <a href="#-list-of-free-ai-providers">providers</a>
34
30
  </p>
35
31
 
36
32
  <p align="center">
@@ -129,6 +125,94 @@ Use ⚡️ Command Palette! with **Ctrl+P**.
129
125
  <img src="https://img.shields.io/badge/USE_%E2%9A%A1%EF%B8%8F%20COMMAND%20PALETTE-CTRL%2BP-22c55e?style=for-the-badge" alt="Use ⚡️ Command Palette with Ctrl+P">
130
126
  </p>
131
127
 
128
+ ---
129
+
130
+ ## 🐳 Docker
131
+
132
+ Run FCM without installing Node.js using the official Docker image:
133
+
134
+ ```bash
135
+ # Quick start (daemon + web UI on port 19280)
136
+ docker run -p 19280:19280 ghcr.io/vava-nessa/free-coding-models:latest
137
+
138
+ # With an API key
139
+ docker run -p 19280:19280 -e OPENROUTER_API_KEY=your_key ghcr.io/vava-nessa/free-coding-models:latest
140
+ ```
141
+
142
+ Access the web dashboard at `http://localhost:19280/` and configure your coding tool to use `http://localhost:19280/v1` with model `fcm`.
143
+
144
+ ### Available Image Tags
145
+
146
+ | Tag | Description |
147
+ |-----|-------------|
148
+ | `latest` | Most recent release |
149
+ | `v{major}.{minor}.{patch}` | Specific version (e.g., `v0.3.70`) |
150
+ | `v{major}.{minor}` | Minor version (e.g., `v0.3`) |
151
+ | `v{major}` | Major version (e.g., `v0`) |
152
+
153
+ ### Environment Variables
154
+
155
+ | Variable | Default | Description |
156
+ |----------|---------|-------------|
157
+ | `FCM_HOST` | `0.0.0.0` | Host to bind to (set `127.0.0.1` for localhost-only) |
158
+ | `FCM_PORT` | `19280` | Port to listen on |
159
+ | `FREE_CODING_MODELS_TELEMETRY` | `0` | Disable telemetry |
160
+
161
+ Provider API keys (all optional):
162
+
163
+ ```bash
164
+ docker run -p 19280:19280 \
165
+ -e NVIDIA_API_KEY=your_key \
166
+ -e GROQ_API_KEY=your_key \
167
+ -e OPENROUTER_API_KEY=your_key \
168
+ ghcr.io/vava-nessa/free-coding-models:latest
169
+ ```
170
+
171
+ ### Docker Compose
172
+
173
+ Create a `docker-compose.yml`:
174
+
175
+ ```yaml
176
+ version: '3.8'
177
+ services:
178
+ fcm:
179
+ image: ghcr.io/vava-nessa/free-coding-models:latest
180
+ container_name: fcm
181
+ restart: unless-stopped
182
+ ports:
183
+ - "19280:19280"
184
+ environment:
185
+ FREE_CODING_MODELS_TELEMETRY: "0"
186
+ FCM_HOST: "0.0.0.0"
187
+ OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-}
188
+ volumes:
189
+ - fcm-data:/home/fcm
190
+ volumes:
191
+ fcm-data:
192
+ ```
193
+
194
+ Run with `docker-compose up -d`. API keys can be passed via a `.env` file or environment variables.
195
+
196
+ ### Troubleshooting
197
+
198
+ **Container won't start:**
199
+ - Check logs: `docker logs fcm`
200
+ - Verify port 19280 is not in use: `docker ps | grep 19280`
201
+
202
+ **Health check fails:**
203
+ - Wait 30s for initial probe cycle
204
+ - Verify API keys are valid: `docker exec fcm curl http://localhost:19280/health`
205
+
206
+ **Cannot connect from host:**
207
+ - Ensure `FCM_HOST=0.0.0.0` (default)
208
+ - Check firewall allows localhost connections
209
+
210
+ **Data persistence:**
211
+ - Config is stored in Docker volume `fcm-data`
212
+ - Recreate the volume with `docker-compose down -v` to reset
213
+
214
+ ---
215
+
132
216
  Need to fix contrast because your terminal theme is fighting the TUI? Press **`G`** at any time to cycle **Auto → Dark → Light**. The switch recolors the full interface live: table, Settings, Help, Smart Recommend, Feedback, and Changelog.
133
217
 
134
218
  **② Pick a model and launch your tool:**
@@ -152,17 +236,8 @@ If the active CLI tool is missing, FCM now catches it before launch, offers a ti
152
236
  ### Common scenarios
153
237
 
154
238
  ```bash
155
- # "I want the most reliable model right now"
156
- free-coding-models --fiable
157
-
158
- # "I want to configure Goose with an S-tier model"
159
- free-coding-models --goose --tier S
160
-
161
- # "I want NVIDIA's top models only"
162
- free-coding-models --origin nvidia --tier S
163
-
164
239
  # "I want the local web dashboard"
165
- free-coding-models --web
240
+ free-coding-models --daemon
166
241
 
167
242
  # "I want one local endpoint that fails over between free models"
168
243
  free-coding-models --daemon-bg
@@ -178,7 +253,14 @@ free-coding-models --tier S --json | jq -r '.[0].modelId'
178
253
  free-coding-models --openclaw --origin groq
179
254
  ```
180
255
 
181
- When launching the web dashboard, `free-coding-models` prefers `http://localhost:3333`. If that port is already used by another app, it now auto-picks the next free local port and prints the exact URL to open.
256
+ When launching the daemon (with `--daemon`), the web dashboard and router API are served from the same port. Configure tools with:
257
+
258
+ | Field | Value |
259
+ |-------|-------|
260
+ | Router Base URL | `http://localhost:19280/v1` |
261
+ | Dashboard URL | `http://localhost:19280/` |
262
+ | Model | `fcm` |
263
+ | API key | `fcm-local` |
182
264
 
183
265
  ### Smart Model Router
184
266
 
@@ -218,9 +300,20 @@ Router endpoints:
218
300
  | `GET /v1/models` | Return virtual models (`fcm`, `fcm:set-name`) |
219
301
  | `GET /health` | Daemon status JSON |
220
302
  | `GET /stats` | Routing, health, request log, and token stats |
221
- | `GET /stream/events` | Live SSE events for dashboard updates |
303
+ | `GET /stream/events` | Live SSE events for router updates |
222
304
  | `POST /daemon/probe-mode` | Set probe mode with `{ "probeMode": "eco" | "balanced" | "aggressive" }` |
223
305
 
306
+ **Web Dashboard endpoints** (served from the same port in `--daemon` mode):
307
+
308
+ | Endpoint | Purpose |
309
+ |----------|---------|
310
+ | `GET /` | Web dashboard HTML |
311
+ | `GET /api/models` | All model data with latency stats |
312
+ | `GET /api/config` | Provider config (keys masked) |
313
+ | `GET /api/events` | Live SSE events for dashboard |
314
+ | `GET /api/key/:provider` | Reveal full API key for provider |
315
+ | `POST /api/settings` | Save API keys and provider toggles |
316
+
224
317
  Routing behavior:
225
318
 
226
319
  - Priority order works immediately on cold start, then probes refine health scores over time.
@@ -253,6 +346,8 @@ Routing behavior:
253
346
  | `--pi` | π Pi |
254
347
  | `--rovo` | 🦘 Rovo Dev CLI |
255
348
  | `--gemini` | ♊ Gemini CLI |
349
+ | `--copilot` | 🤖 Copilot CLI |
350
+ | `--forgecode` | 🔥 ForgeCode |
256
351
 
257
352
  Press **`Z`** in the TUI to cycle between tools without restarting.
258
353
 
@@ -507,6 +602,7 @@ Telemetry is enabled by default and can be disabled with any of the following:
507
602
  <td align="center" width="120"><a href="https://github.com/PhucTruong-ctrl"><img src="https://github.com/PhucTruong-ctrl.png?s=80" width="80" height="80" style="border-radius:50%" alt="PhucTruong-ctrl"></a></td>
508
603
  <td align="center" width="120"><a href="https://github.com/chindris-mihai-alexandru"><img src="https://avatars.githubusercontent.com/u/12643176?v=4&s=80" width="80" height="80" style="border-radius:50%" alt="chindris-mihai-alexandru"></a></td>
509
604
  <td align="center" width="120"><a href="https://github.com/serajbaltu"><img src="https://avatars.githubusercontent.com/u/90699173?v=4&s=80" width="80" height="80" style="border-radius:50%" alt="serajbaltu"></a></td>
605
+ <td align="center" width="120"><a href="https://github.com/stgreenb"><img src="https://avatars.githubusercontent.com/u/18483964?v=4&s=80" width="80" height="80" style="border-radius:50%" alt="stgreenb"></a></td>
510
606
  </tr>
511
607
  <tr>
512
608
  <td align="center"><a href="https://github.com/vava-nessa"><sub><b>vava-nessa</b></sub></a></td>
@@ -516,6 +612,7 @@ Telemetry is enabled by default and can be disabled with any of the following:
516
612
  <td align="center"><a href="https://github.com/PhucTruong-ctrl"><sub><b>PhucTruong-ctrl</b></sub></a></td>
517
613
  <td align="center"><a href="https://github.com/chindris-mihai-alexandru"><sub><b>chindris-mihai-alexandru</b></sub></a></td>
518
614
  <td align="center"><a href="https://github.com/serajbaltu"><sub><b>serajbaltu</b></sub></a></td>
615
+ <td align="center"><a href="https://github.com/stgreenb"><sub><b>stgreenb</b></sub></a></td>
519
616
  </tr>
520
617
  </table>
521
618
 
@@ -92,15 +92,7 @@ async function main() {
92
92
  process.exit(1);
93
93
  }
94
94
 
95
- // 📖 --web mode: launch the web dashboard instead of the TUI
96
- if (cliArgs.webMode) {
97
- const { startWebServer } = await import('../web/server.js')
98
- const port = parseInt(process.env.FCM_PORT || '3333', 10)
99
- await startWebServer(port, { open: true })
100
- return
101
- }
102
-
103
- // 📖 Load JSON config
95
+ // Load JSON config
104
96
  const config = loadConfig();
105
97
  ensureTelemetryConfig(config);
106
98
  ensureFavoritesConfig(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.3.65",
3
+ "version": "0.3.67",
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",
@@ -68,10 +68,9 @@
68
68
  },
69
69
  "devDependencies": {
70
70
  "@vitejs/plugin-react": "^6.0.1",
71
- "agent-tui": "^1.0.1",
72
- "react": "^19.2.4",
73
- "react-dom": "^19.2.4",
74
- "vite": "^8.0.5",
71
+ "react": "^19.2.6",
72
+ "react-dom": "^19.2.6",
73
+ "vite": "^8.0.13",
75
74
  "vite-plus": "^0.1.16"
76
75
  }
77
76
  }
package/sources.js CHANGED
@@ -46,15 +46,10 @@ export const nvidiaNim = [
46
46
  ['moonshotai/kimi-k2.6', 'Kimi K2.6', 'S+', '76.8%', '256k'],
47
47
  ['deepseek-ai/deepseek-v4-pro', 'DeepSeek V4 Pro', 'S+', '73.1%', '128k'],
48
48
  ['deepseek-ai/deepseek-v4-flash', 'DeepSeek V4 Flash', 'S+', '72.0%', '128k'],
49
- ['z-ai/glm4.7', 'GLM 4.7', 'S+', '73.8%', '200k'],
50
- ['moonshotai/kimi-k2-thinking', 'Kimi K2 Thinking', 'S+', '71.3%', '256k'], // ⚠️ Deprecation pending
51
- ['minimaxai/minimax-m2.5', 'MiniMax M2.5', 'S+', '80.2%', '200k'],
49
+ ['z-ai/glm5', 'GLM 5', 'S+', '73.8%', '200k'],
52
50
  ['stepfun-ai/step-3.5-flash', 'Step 3.5 Flash', 'S+', '74.4%', '256k'],
53
51
  ['qwen/qwen3-coder-480b-a35b-instruct', 'Qwen3 Coder 480B', 'S+', '70.6%', '256k'],
54
- ['mistralai/devstral-2-123b-instruct-2512', 'Devstral 2 123B', 'S+', '72.2%', '256k'],
55
52
  // ── S tier — SWE-bench Verified 60–70% ──
56
- ['moonshotai/kimi-k2-instruct-0905', 'Kimi K2 Instruct 0905', 'S', '65.8%', '256k'],
57
- ['moonshotai/kimi-k2-instruct', 'Kimi K2 Instruct', 'S', '65.8%', '128k'],
58
53
  ['minimaxai/minimax-m2', 'MiniMax M2', 'S', '69.4%', '128k'],
59
54
  ['qwen/qwen3-next-80b-a3b-thinking', 'Qwen3 80B Thinking', 'S', '68.0%', '128k'],
60
55
  ['qwen/qwen3-next-80b-a3b-instruct', 'Qwen3 80B Instruct', 'S', '65.0%', '128k'],
@@ -70,13 +65,9 @@ export const nvidiaNim = [
70
65
  ['nvidia/nemotron-3-super-120b-a12b', 'Nemotron 3 Super', 'A+', '56.0%', '128k'],
71
66
  ['nvidia/nemotron-3-nano-omni-30b-a3b-reasoning','Nemotron 3 Omni', 'A+', '52.0%', '128k'],
72
67
  // ── A tier — SWE-bench Verified 40–50% ──
73
- ['mistralai/mistral-medium-3-instruct', 'Mistral Medium 3', 'A', '48.0%', '128k'],
74
- ['mistralai/magistral-small-2506', 'Magistral Small', 'A', '45.0%', '32k'],
75
68
  ['nvidia/llama-3.3-nemotron-super-49b-v1.5', 'Nemotron Super 49B', 'A', '49.0%', '128k'],
76
69
  ['nvidia/nemotron-3-nano-30b-a3b', 'Nemotron Nano 30B', 'A', '43.0%', '128k'],
77
70
  ['openai/gpt-oss-20b', 'GPT OSS 20B', 'A', '42.0%', '128k'],
78
- ['qwen/qwen2.5-coder-32b-instruct', 'Qwen2.5 Coder 32B', 'A', '46.0%', '32k'],
79
- ['meta/llama-3.1-405b-instruct', 'Llama 3.1 405B', 'A', '44.0%', '128k'],
80
71
  ['google/gemma-4-31b-it', 'Gemma 4 31B', 'A', '45.0%', '256k'],
81
72
  // ── A- tier — SWE-bench Verified 35–40% ──
82
73
  ['meta/llama-3.3-70b-instruct', 'Llama 3.3 70B', 'A-', '39.5%', '128k'],
package/src/app.js CHANGED
@@ -274,6 +274,8 @@ export async function runApp(cliArgs, config) {
274
274
  pi: cliArgs.piMode,
275
275
  rovo: cliArgs.rovoMode,
276
276
  gemini: cliArgs.geminiMode,
277
+ copilot: cliArgs.copilotMode,
278
+ forgecode: cliArgs.forgecodeMode,
277
279
  }
278
280
  return flagByMode[toolMode] === true
279
281
  })
package/src/cli-help.js CHANGED
@@ -36,8 +36,7 @@ const ANALYSIS_FLAGS = [
36
36
  ]
37
37
 
38
38
  const CONFIG_FLAGS = [
39
- { flag: '--web', description: 'Launch the web dashboard in your browser' },
40
- { flag: '--daemon', description: 'Start the FCM Router daemon in the foreground' },
39
+ { flag: '--daemon', description: 'Start the FCM Router daemon + web dashboard (same port)' },
41
40
  { flag: '--daemon-bg', description: 'Start the FCM Router daemon in the background' },
42
41
  { flag: '--daemon-status', description: 'Print FCM Router daemon status JSON' },
43
42
  { flag: '--daemon-stop', description: 'Gracefully stop the FCM Router daemon' },
@@ -48,7 +47,7 @@ const CONFIG_FLAGS = [
48
47
 
49
48
  const EXAMPLES = [
50
49
  'free-coding-models --help',
51
- 'free-coding-models --web',
50
+ 'free-coding-models --daemon',
52
51
  'free-coding-models --daemon-bg',
53
52
  'free-coding-models --daemon-status',
54
53
  'free-coding-models --sync-set',
@@ -52,7 +52,7 @@ import { getToolMeta } from './tool-metadata.js'
52
52
  const DIRECT_INSTALL_UNSUPPORTED_PROVIDERS = new Set(['replicate', 'zai', 'rovo', 'gemini', 'opencode-zen'])
53
53
  // 📖 Install Endpoints only lists tools whose persisted config shape is actually supported here.
54
54
  // 📖 Claude Code, Codex, and Gemini stay out while their dedicated bridges are being rebuilt.
55
- const INSTALL_TARGET_MODES = ['opencode', 'opencode-desktop', 'opencode-web', 'openclaw', 'kilo', 'crush', 'goose', 'pi', 'aider', 'qwen', 'openhands', 'amp', 'hermes', 'continue', 'cline', 'fcm_router']
55
+ const INSTALL_TARGET_MODES = ['opencode', 'opencode-desktop', 'opencode-web', 'openclaw', 'kilo', 'crush', 'goose', 'pi', 'aider', 'qwen', 'openhands', 'amp', 'hermes', 'continue', 'cline', 'forgecode', 'fcm_router']
56
56
 
57
57
  function getDefaultPaths() {
58
58
  const home = homedir()
@@ -67,6 +67,7 @@ function getDefaultPaths() {
67
67
  aiderConfigPath: join(home, '.aider.conf.yml'),
68
68
  ampConfigPath: join(home, '.config', 'amp', 'settings.json'),
69
69
  qwenConfigPath: join(home, '.qwen', 'settings.json'),
70
+ forgeCodeConfigPath: join(home, '.forge', '.forge.toml'),
70
71
  }
71
72
  }
72
73
 
@@ -526,6 +527,65 @@ function installIntoEnvBasedTool(providerKey, models, apiKey, toolMode) {
526
527
  return { path: envFilePath, backupPath, providerId, modelCount: models.length }
527
528
  }
528
529
 
530
+ // 📖 installIntoForgeCode: writes a managed [[providers]] block into ~/.forge/.forge.toml.
531
+ // 📖 ForgeCode uses TOML config with [[providers]] entries for custom OpenAI-compatible endpoints.
532
+ // 📖 Each provider gets one [[providers]] entry with the model catalog noted in comments.
533
+ // 📖 The API key is referenced via an env var so ForgeCode picks it up at runtime.
534
+ function installIntoForgeCode(providerKey, models, apiKey, paths) {
535
+ const filePath = paths.forgeCodeConfigPath
536
+ const providerId = getManagedProviderId(providerKey)
537
+ const secretEnvName = `FCM_${providerKey.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_API_KEY`
538
+ const baseUrl = resolveProviderBaseUrl(providerKey)
539
+
540
+ if (!baseUrl) {
541
+ throw new Error(`Cannot resolve base URL for ${getProviderLabel(providerKey)}`)
542
+ }
543
+
544
+ // 📖 Ensure the API key is in env for ForgeCode to use
545
+ process.env[secretEnvName] = apiKey
546
+
547
+ const completionsUrl = baseUrl.endsWith('/chat/completions') ? baseUrl : `${baseUrl}/chat/completions`
548
+
549
+ // 📖 Read existing content
550
+ let content = ''
551
+ if (existsSync(filePath)) {
552
+ content = readFileSync(filePath, 'utf8')
553
+ }
554
+
555
+ // 📖 Remove any previous FCM-managed provider block for this provider
556
+ const markerStart = `# >>> FCM managed provider: ${providerId}`
557
+ const markerEnd = `# <<< FCM managed provider: ${providerId}`
558
+ const markerRegex = new RegExp(
559
+ `\\n?${markerStart.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${markerEnd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n?`,
560
+ 'g'
561
+ )
562
+ content = content.replace(markerRegex, '\n')
563
+
564
+ // 📖 Build a fresh [[providers]] TOML block with model catalog comments
565
+ const modelComments = models.map(m => `# 📖 Model: ${m.label} (${m.modelId}) — ${m.tier}`).join('\n')
566
+ const providerBlock = [
567
+ '',
568
+ markerStart,
569
+ `# 📖 Provider: ${getManagedProviderLabel(providerKey)} (${models.length} models)`,
570
+ modelComments,
571
+ '[[providers]]',
572
+ `id = "${providerId}"`,
573
+ `url = "${completionsUrl}"`,
574
+ `api_key_vars = "${secretEnvName}"`,
575
+ 'response_type = "OpenAI"',
576
+ 'auth_methods = ["api_key"]',
577
+ markerEnd,
578
+ ].join('\n')
579
+
580
+ content = content.trimEnd() + '\n' + providerBlock + '\n'
581
+
582
+ ensureDirFor(filePath)
583
+ const backupPath = backupIfExists(filePath)
584
+ writeFileSync(filePath, content)
585
+
586
+ return { path: filePath, backupPath, providerId, modelCount: models.length }
587
+ }
588
+
529
589
  // 📖 installIntoFcmRouter: adds provider endpoints to the running FCM Router daemon
530
590
  // 📖 via the /sets API so the router can use them for failover routing.
531
591
  // 📖 Uses the daemon's expected schema: { provider, model, priority } per model entry.
@@ -605,6 +665,8 @@ export function installProviderEndpoints(config, providerKey, toolMode, options
605
665
  installResult = installIntoEnvBasedTool(providerKey, models, apiKey, canonicalToolMode, paths)
606
666
  } else if (canonicalToolMode === 'fcm_router') {
607
667
  installResult = installIntoFcmRouter(providerKey, models, apiKey)
668
+ } else if (canonicalToolMode === 'forgecode') {
669
+ installResult = installIntoForgeCode(providerKey, models, apiKey, paths)
608
670
  } else {
609
671
  throw new Error(`Unsupported install target: ${toolMode}`)
610
672
  }
@@ -415,7 +415,7 @@ export function createKeyHandler(ctx) {
415
415
 
416
416
  async function launchSelectedModel(selected, options = {}) {
417
417
  const { uiAlreadyStopped = false } = options
418
- userSelected = { modelId: selected.modelId, label: selected.label, tier: selected.tier, providerKey: selected.providerKey }
418
+ userSelected = { modelId: selected.modelId, label: selected.label, tier: selected.tier, providerKey: selected.providerKey, ctx: selected.ctx }
419
419
 
420
420
  if (!uiAlreadyStopped) {
421
421
  readline.emitKeypressEvents(process.stdin)