free-coding-models 0.3.64 → 0.3.66

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,15 @@
1
- ## [0.3.64] - 2026-05-06
1
+ ## [0.3.66] - 2026-05-16
2
2
 
3
- ### Fixed
4
-
5
- - **E footer spacing and color** Fixed the active `E` footer label so the hotkey letter keeps its distinct hotkey color and is separated from the active filter text by a readable space. This makes the active filter state easier to scan in the TUI footer.
3
+ ### Added
4
+ - **ForgeCode Integration**: You can now seamlessly launch and install provider endpoints directly into ForgeCode's TOML config. Use the `--forgecode` flag to run it.
5
+ - **GitHub Copilot CLI Support**: Added direct integration with GitHub Copilot CLI (`--copilot`), dynamically setting `COPILOT_*` environment variables for seamless Bring Your Own Key (BYOK) execution.
6
+ - **Security Audit Report**: Added comprehensive security audit documentation from Jules.
6
7
 
7
8
  ### Changed
9
+ - **NVIDIA NIM Model Catalog Update**: Removed 11 deprecated models and updated GLM to the new `glm5` to keep the catalog fresh and fully operational.
10
+ - **README Optimization**: Refactored README layout and updated image sizes for a cleaner documentation experience.
11
+ - **Testing Architecture Refactoring**: Replaced `agent-tui` with native `tmux` to streamline visual TUI testing.
8
12
 
9
- - **Clearer E filter names** — Renamed the `E` cycle from `Working only` / `Best mode` to `Configured only` / `Usable only`. The behavior is unchanged: `Configured only` keeps configured providers visible, while `Usable only` narrows the table to models with healthy status and usable verdicts.
10
- - **README TUI key reference** Updated the `E` shortcut documentation to describe the full visibility cycle: `Active only → Configured only → Usable only`.
13
+ ### Fixed
14
+ - **Repository Maintenance**: Cleaned up obsolete tooling artifacts to maintain a bloat-free codebase.
15
+ - **Dependencies**: Bumped internal UI framework tools (React `19.2.6`, Vite `8.0.13`).
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">
@@ -253,6 +249,8 @@ Routing behavior:
253
249
  | `--pi` | π Pi |
254
250
  | `--rovo` | 🦘 Rovo Dev CLI |
255
251
  | `--gemini` | ♊ Gemini CLI |
252
+ | `--copilot` | 🤖 Copilot CLI |
253
+ | `--forgecode` | 🔥 ForgeCode |
256
254
 
257
255
  Press **`Z`** in the TUI to cycle between tools without restarting.
258
256
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.3.64",
3
+ "version": "0.3.66",
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
  })
@@ -776,33 +778,35 @@ export async function runApp(cliArgs, config) {
776
778
  const activeVerdict = VERDICT_CYCLE[state.verdictFilterMode]
777
779
  const activeHealth = HEALTH_CYCLE[state.healthFilterMode]
778
780
  state.results.forEach(r => {
779
- // 📖 Sticky-favorites mode keeps favorites visible regardless of configured-only, tier, or provider filters.
780
- if (state.favoritesPinnedAndSticky && r.isFavorite) {
781
- r.hidden = false
782
- return
783
- }
781
+ const stickyFavorite = state.favoritesPinnedAndSticky && r.isFavorite
784
782
  // 📖 CLI-only tools (rovo, gemini) and Zen models don't need traditional API keys —
785
783
  // 📖 they authenticate via their own CLI login flow, so "configured only" should never hide them.
786
784
  const providerMeta = PROVIDER_METADATA[r.providerKey]
787
785
  const noKeyNeeded = providerMeta?.cliOnly || providerMeta?.zenOnly
788
- // 📖 E toggles "Show only configured & working models":
789
- // 📖 hide models where provider has no key, or where the health status is noauth/auth_error (but keep timeout and 429)
790
- const badHealth = r.status === 'noauth' || r.status === 'auth_error'
791
- const unconfiguredHide = state.hideUnconfiguredModels && !noKeyNeeded && (!getApiKey(state.config, r.providerKey) || badHealth)
792
- if (unconfiguredHide) {
793
- r.hidden = true
794
- return
795
- }
796
- // 📖 Usable only: only show models with Health UP and Verdict Perfect/Normal/Slow
797
- if (state.bestModeOnly) {
798
- const bmVerdict = getVerdict(r)
799
- const bmVerdictOk = ['Perfect', 'Normal', 'Slow'].includes(bmVerdict)
800
- const bmHealthOk = r.status === 'up'
801
- if (!bmHealthOk || !bmVerdictOk) {
802
- r.hidden = true
803
- return
804
- }
805
- }
786
+ // 📖 E toggles "Show only configured & working models":
787
+ // 📖 hide models where provider has no key, or where the health status is noauth/auth_error (but keep timeout and 429)
788
+ const badHealth = r.status === 'noauth' || r.status === 'auth_error'
789
+ const unconfiguredHide = state.hideUnconfiguredModels && !noKeyNeeded && (!getApiKey(state.config, r.providerKey) || badHealth)
790
+ if (unconfiguredHide) {
791
+ r.hidden = true
792
+ return
793
+ }
794
+ // 📖 Usable only: only show models with Health UP and Verdict Perfect/Normal/Slow
795
+ if (state.bestModeOnly) {
796
+ const bmVerdict = getVerdict(r)
797
+ const bmVerdictOk = ['Perfect', 'Normal', 'Slow'].includes(bmVerdict)
798
+ const bmHealthOk = r.status === 'up'
799
+ if (!bmHealthOk || !bmVerdictOk) {
800
+ r.hidden = true
801
+ return
802
+ }
803
+ }
804
+ // 📖 Sticky-favorites mode keeps usable favorites visible regardless of
805
+ // 📖 tier/provider/text filters, but "Usable only" health still wins above.
806
+ if (stickyFavorite) {
807
+ r.hidden = false
808
+ return
809
+ }
806
810
  // 📖 Apply tier, origin, verdict, and health filters — model is hidden if it fails any
807
811
  const allowedTiers = (activeTier && TIER_LETTER_MAP[activeTier]) ? TIER_LETTER_MAP[activeTier] : [activeTier]
808
812
  const tierHide = activeTier !== null && !allowedTiers.includes(r.tier)
@@ -1057,12 +1061,17 @@ if (state.bestModeOnly) {
1057
1061
  try {
1058
1062
  refreshAutoPingMode()
1059
1063
  state.frame++
1064
+ // 📖 Re-apply live health filters each frame so "Usable only" truly means
1065
+ // 📖 usable right now: models enter/leave as soon as ping status changes.
1066
+ applyTierFilter()
1060
1067
  // 📖 Cache visible+sorted models each frame so Enter handler always matches the display
1061
1068
  if (!state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.incompatibleFallbackOpen && !state.recommendOpen && !state.changelogOpen && !state.installedModelsOpen && !state.routerDashboardOpen && !state.commandPaletteOpen) {
1062
1069
  const visible = state.results.filter(r => !r.hidden)
1063
1070
  state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
1064
1071
  pinFavorites: state.favoritesPinnedAndSticky,
1065
1072
  })
1073
+ if (state.cursor >= state.visibleSorted.length) state.cursor = Math.max(0, state.visibleSorted.length - 1)
1074
+ adjustScrollOffset(state)
1066
1075
  }
1067
1076
  const tableTerminalRows = state.terminalRows
1068
1077
 
@@ -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)
@@ -304,6 +304,28 @@ export const TOOL_BOOTSTRAP_METADATA = {
304
304
  },
305
305
  },
306
306
  },
307
+ copilot: {
308
+ binary: 'copilot',
309
+ docsUrl: 'https://github.com/github/copilot',
310
+ install: {
311
+ default: {
312
+ shellCommand: 'npm install -g @github/copilot',
313
+ summary: 'Install GitHub Copilot CLI globally via npm.',
314
+ note: 'After installation, run `copilot` to authenticate with GitHub.',
315
+ },
316
+ },
317
+ },
318
+ forgecode: {
319
+ binary: 'forge',
320
+ docsUrl: 'https://forgecode.dev',
321
+ install: {
322
+ default: {
323
+ shellCommand: 'npm install -g forgecode',
324
+ summary: 'Install ForgeCode globally via npm.',
325
+ note: 'After installation, run `forge` to start. Use `forge provider login` to set up credentials.',
326
+ },
327
+ },
328
+ },
307
329
  }
308
330
 
309
331
  export function getToolBootstrapMeta(mode) {
@@ -20,6 +20,7 @@
20
20
  * 📖 Hermes: uses `hermes config set` CLI commands + `hermes gateway restart` before launching `hermes chat`
21
21
  * 📖 Continue: writes ~/.continue/config.yaml with provider: openai + apiBase
22
22
  * 📖 Cline: writes ~/.cline/globalState.json with openai-compatible provider config
23
+ * 📖 ForgeCode: writes [[providers]] TOML block into ~/.forge/.forge.toml + sets [session] defaults
23
24
  *
24
25
  * @functions
25
26
  * → `resolveLauncherModelId` — choose the provider-specific id for a launch
@@ -64,6 +65,19 @@ function ensureDir(filePath) {
64
65
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
65
66
  }
66
67
 
68
+ // 📖 Parse a context window string (e.g. "128k", "1M", "32k") to token count number.
69
+ function parseCtxToTokens(ctx) {
70
+ if (!ctx || typeof ctx !== 'string') return null
71
+ const match = ctx.match(/^\s*(\d+(?:\.\d+)?)\s*([kKmM]?)\s*$/)
72
+ if (!match) return null
73
+ const num = parseFloat(match[1])
74
+ const suffix = match[2].toLowerCase()
75
+ // 📖 LLM token counts use binary (1024), not decimal (1000).
76
+ if (suffix === 'k') return Math.round(num * 1024)
77
+ if (suffix === 'm') return Math.round(num * 1024 * 1024)
78
+ return Math.round(num)
79
+ }
80
+
67
81
  function getDefaultToolPaths(homeDir = homedir()) {
68
82
  return {
69
83
  aiderConfigPath: join(homeDir, '.aider.conf.yml'),
@@ -79,6 +93,7 @@ function getDefaultToolPaths(homeDir = homedir()) {
79
93
  hermesConfigPath: join(homeDir, '.hermes', 'config.yaml'),
80
94
  continueConfigPath: join(homeDir, '.continue', 'config.yaml'),
81
95
  clineConfigPath: join(homeDir, '.cline', 'globalState.json'),
96
+ forgeCodeConfigPath: join(homeDir, '.forge', '.forge.toml'),
82
97
  }
83
98
  }
84
99
 
@@ -504,6 +519,86 @@ function writeHermesConfig(model, apiKey, baseUrl, paths = getDefaultToolPaths()
504
519
  return { filePath: configPath, backupPath }
505
520
  }
506
521
 
522
+ // 📖 writeForgeCodeConfig — write a managed [[providers]] block into ~/.forge/.forge.toml.
523
+ // 📖 ForgeCode uses TOML config with [[providers]] entries for custom OpenAI-compatible endpoints.
524
+ // 📖 Strategy:
525
+ // 📖 1. Read the existing .forge.toml (if any)
526
+ // 📖 2. Strip any previous FCM-managed provider block (delimited by comments)
527
+ // 📖 3. Append a fresh [[providers]] block with the selected model's provider details
528
+ // 📖 4. Update or insert [session] defaults to auto-select the model on next `forge` launch
529
+ // 📖 The provider ID uses the `fcm-{providerKey}` namespace to avoid clobbering user-defined providers.
530
+ // 📖 The API key is referenced via an env var (FCM_{PROVIDER}_API_KEY) and also set in the process env.
531
+ function writeForgeCodeConfig(model, apiKey, baseUrl, providerKey, paths = getDefaultToolPaths()) {
532
+ const filePath = paths.forgeCodeConfigPath
533
+ const backupPath = backupIfExists(filePath)
534
+ const providerId = `fcm-${providerKey}`
535
+ const providerLabel = PROVIDER_METADATA[providerKey]?.label || sources[providerKey]?.name || providerKey
536
+ const secretEnvName = `FCM_${providerKey.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_API_KEY`
537
+
538
+ // 📖 Ensure the API key is available in env for ForgeCode to pick up
539
+ process.env[secretEnvName] = apiKey
540
+
541
+ // 📖 Build the provider's chat completions URL
542
+ const completionsUrl = baseUrl
543
+ ? (baseUrl.endsWith('/chat/completions') ? baseUrl : `${baseUrl}/chat/completions`)
544
+ : ''
545
+
546
+ // 📖 Read existing TOML content (if any)
547
+ let content = ''
548
+ if (existsSync(filePath)) {
549
+ content = readFileSync(filePath, 'utf8')
550
+ }
551
+
552
+ // 📖 Remove any previous FCM-managed provider block (between marker comments)
553
+ const markerStart = `# >>> FCM managed provider: ${providerId}`
554
+ const markerEnd = `# <<< FCM managed provider: ${providerId}`
555
+ const markerRegex = new RegExp(
556
+ `\\n?${markerStart.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${markerEnd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n?`,
557
+ 'g'
558
+ )
559
+ content = content.replace(markerRegex, '\n')
560
+
561
+ // 📖 Build a fresh [[providers]] TOML block
562
+ const providerBlock = [
563
+ '',
564
+ markerStart,
565
+ '[[providers]]',
566
+ `id = "${providerId}"`,
567
+ `url = "${completionsUrl}"`,
568
+ `api_key_vars = "${secretEnvName}"`,
569
+ 'response_type = "OpenAI"',
570
+ 'auth_methods = ["api_key"]',
571
+ markerEnd,
572
+ ].join('\n')
573
+
574
+ content = content.trimEnd() + '\n' + providerBlock + '\n'
575
+
576
+ // 📖 Update or insert [session] defaults so ForgeCode auto-selects this model
577
+ const sessionProviderLine = `provider_id = "${providerId}"`
578
+ const sessionModelLine = `model_id = "${model.modelId}"`
579
+
580
+ if (/^\[session\]/m.test(content)) {
581
+ // 📖 Replace existing provider_id/model_id under [session]
582
+ if (/^provider_id\s*=/m.test(content)) {
583
+ content = content.replace(/^provider_id\s*=.*$/m, sessionProviderLine)
584
+ } else {
585
+ content = content.replace(/^\[session\]/m, `[session]\n${sessionProviderLine}`)
586
+ }
587
+ if (/^model_id\s*=/m.test(content)) {
588
+ content = content.replace(/^model_id\s*=.*$/m, sessionModelLine)
589
+ } else {
590
+ content = content.replace(/^\[session\]/m, `[session]\n${sessionModelLine}`)
591
+ }
592
+ } else {
593
+ // 📖 No [session] block — append one
594
+ content = content.trimEnd() + '\n\n[session]\n' + sessionProviderLine + '\n' + sessionModelLine + '\n'
595
+ }
596
+
597
+ ensureDir(filePath)
598
+ writeFileSync(filePath, content)
599
+ return { filePath, backupPath }
600
+ }
601
+
507
602
  // 📖 restartHermesGateway — restart the Hermes messaging gateway after config changes.
508
603
  // 📖 Non-blocking: if gateway is not running, this is a no-op.
509
604
  function restartHermesGateway() {
@@ -833,6 +928,45 @@ export function prepareExternalToolLaunch(mode, model, config, options = {}) {
833
928
  }
834
929
  }
835
930
 
931
+ if (mode === 'copilot') {
932
+ // 📖 copilot: set BYOK env vars so copilot uses the selected provider/model
933
+ const copilotModelId = resolveLauncherModelId(model)
934
+ env.COPILOT_PROVIDER_BASE_URL = baseUrl
935
+ env.COPILOT_MODEL = copilotModelId
936
+ if (apiKey) env.COPILOT_PROVIDER_API_KEY = apiKey
937
+
938
+ // 📖 Set context window limits from model data
939
+ const promptTokens = parseCtxToTokens(model.ctx)
940
+ if (promptTokens) env.COPILOT_PROVIDER_MAX_PROMPT_TOKENS = String(promptTokens)
941
+ // 📖 16k max output as a safety cap — most S+/S tier coding models
942
+ // 📖 support 16-32k output. copilot falls back to built-in model
943
+ // 📖 catalog defaults when a model ID is recognized.
944
+ env.COPILOT_PROVIDER_MAX_OUTPUT_TOKENS = '16384'
945
+
946
+ return {
947
+ command: 'copilot',
948
+ args: [],
949
+ env,
950
+ apiKey,
951
+ baseUrl,
952
+ meta,
953
+ configArtifacts: [],
954
+ }
955
+ }
956
+
957
+ if (mode === 'forgecode') {
958
+ const result = writeForgeCodeConfig(model, apiKey, baseUrl, model.providerKey, paths)
959
+ return {
960
+ command: 'forge',
961
+ args: [],
962
+ env,
963
+ apiKey,
964
+ baseUrl,
965
+ meta,
966
+ configArtifacts: [{ path: result.filePath, backupPath: result.backupPath, label: 'config' }],
967
+ }
968
+ }
969
+
836
970
  return {
837
971
  blocked: true,
838
972
  exitCode: 1,
@@ -934,6 +1068,16 @@ export async function startExternalTool(mode, model, config) {
934
1068
  return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
935
1069
  }
936
1070
 
1071
+ if (mode === 'copilot') {
1072
+ console.log(chalk.dim(` 📖 Copilot CLI configured with model: ${model.modelId}`))
1073
+ return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
1074
+ }
1075
+
1076
+ if (mode === 'forgecode') {
1077
+ console.log(chalk.dim(` 📖 ForgeCode configured with model: ${model.modelId}`))
1078
+ return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
1079
+ }
1080
+
937
1081
  console.log(chalk.red(` X Unsupported external tool mode: ${mode}`))
938
1082
  return 1
939
1083
  }
@@ -46,6 +46,8 @@ export const TOOL_METADATA = {
46
46
  jcode: { label: 'jcode', emoji: '🪼', flag: '--jcode', color: [255, 140, 0] },
47
47
  xcode: { label: 'Xcode Intelligence',emoji: '🛠️', flag: '--xcode', color: [20, 126, 251] },
48
48
  fcm_router: { label: 'FCM Router', emoji: '🧭', flag: '--fcm-router', color: [80, 200, 120] },
49
+ copilot: { label: 'Copilot CLI', emoji: '🤖', flag: '--copilot', color: [200, 220, 255] },
50
+ forgecode: { label: 'ForgeCode', emoji: '🔥', flag: '--forgecode', color: [255, 120, 50] },
49
51
  }
50
52
 
51
53
  // 📖 Deduplicated emoji order for the "Compatible with" column.
@@ -64,13 +66,14 @@ export const COMPAT_COLUMN_SLOTS = [
64
66
  { emoji: '⚡', toolKeys: ['amp'], color: [255, 232, 98] },
65
67
  { emoji: '🔮', toolKeys: ['hermes'], color: [200, 160, 255] },
66
68
  { emoji: '▶️', toolKeys: ['continue'], color: [255, 100, 100] },
67
- { emoji: '🧠', toolKeys: ['cline'], color: [100, 220, 180] },
69
+ { emoji: '🧠', toolKeys: ['cline'], color: [100, 220, 180] },
68
70
  { emoji: '🧭', toolKeys: ['fcm_router'], color: [80, 200, 120] },
69
71
  { emoji: '🦘', toolKeys: ['rovo'], color: [148, 163, 184] },
70
72
  { emoji: '♊', toolKeys: ['gemini'], color: [66, 165, 245] },
71
73
  { emoji: '🪼', toolKeys: ['jcode'], color: [255, 140, 0] },
72
74
  { emoji: '🛠️', toolKeys: ['xcode'], color: [20, 126, 251] },
73
- { emoji: '🧭', toolKeys: ['fcm_router'], color: [80, 200, 120] },
75
+ { emoji: '🤖', toolKeys: ['copilot'], color: [200, 220, 255] },
76
+ { emoji: '🔥', toolKeys: ['forgecode'], color: [255, 120, 50] },
74
77
  ]
75
78
 
76
79
  export const TOOL_MODE_ORDER = [
@@ -94,6 +97,8 @@ export const TOOL_MODE_ORDER = [
94
97
  'fcm_router',
95
98
  'rovo',
96
99
  'gemini',
100
+ 'copilot',
101
+ 'forgecode',
97
102
  ]
98
103
 
99
104
  export function getToolMeta(mode) {
package/src/utils.js CHANGED
@@ -389,14 +389,16 @@ export function findBestModel(results) {
389
389
  // - API key: first positional arg that does not look like a CLI flag (e.g., "nvapi-xxx")
390
390
  // - Boolean flags: --best, --fiable, --opencode, --opencode-desktop, --opencode-web, --openclaw,
391
391
  // --aider, --crush, --goose, --qwen, --kilo,
392
- // --openhands, --amp, --pi, --daemon, --daemon-bg, --daemon-stop,
392
+ // --openhands, --amp, --pi, --rovo, --hermes, --continue, --cline,
393
+ // --xcode, --gemini, --jcode, --copilot, --forgecode,
394
+ // --daemon, --daemon-bg, --daemon-stop,
393
395
  // --daemon-status, --no-telemetry, --json, --help/-h (case-insensitive)
394
396
  // - Value flag: --tier <letter> (the next non-flag arg is the tier value)
395
397
  //
396
398
  // Returns:
397
399
  // { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openCodeWebMode, openClawMode,
398
400
  // aiderMode, crushMode, gooseMode, qwenMode, openHandsMode, ampMode,
399
- // piMode, jcodeMode, noTelemetry, jsonMode, helpMode, tierFilter }
401
+ // piMode, jcodeMode, copilotMode, forgecodeMode, noTelemetry, jsonMode, helpMode, tierFilter }
400
402
  //
401
403
  // 📖 Note: apiKey may be null here — the main CLI falls back to env vars and saved config.
402
404
  export function parseArgs(argv) {
@@ -471,6 +473,8 @@ export function parseArgs(argv) {
471
473
  const xcodeMode = flags.includes('--xcode')
472
474
  const geminiMode = flags.includes('--gemini')
473
475
  const jcodeMode = flags.includes('--jcode')
476
+ const copilotMode = flags.includes('--copilot')
477
+ const forgecodeMode = flags.includes('--forgecode')
474
478
  const noTelemetry = flags.includes('--no-telemetry')
475
479
  const devMode = flags.includes('--dev')
476
480
  const jsonMode = flags.includes('--json')
@@ -528,6 +532,8 @@ export function parseArgs(argv) {
528
532
  rovoMode,
529
533
  geminiMode,
530
534
  jcodeMode,
535
+ copilotMode,
536
+ forgecodeMode,
531
537
  noTelemetry,
532
538
  jsonMode,
533
539
  helpMode,