free-coding-models 0.2.15 → 0.2.17
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 +25 -0
- package/README.md +20 -11
- package/bin/free-coding-models.js +7 -3
- package/package.json +1 -1
- package/src/endpoint-installer.js +161 -1
- package/src/key-handler.js +28 -2
- package/src/overlays.js +95 -43
- package/src/tool-metadata.js +14 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## 0.2.17
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **All coding tools re-enabled in Z-cycle**: Aider, Claude Code, Codex CLI, Gemini CLI, Qwen Code, OpenHands, and Amp are now back in the public tool mode cycle alongside OpenCode, OpenClaw, Crush, Goose, and Pi — 13 tools total.
|
|
9
|
+
- **All coding tools available in Install Endpoints (Y key)**: The endpoint installer now supports all 13 tools as install targets, not just the original 5. Each tool gets its proper config format (JSON, YAML, or env file).
|
|
10
|
+
- **Connection mode choice in Install flow**: When installing endpoints (Y key), users now choose between **Direct Provider** (pure API connection) or **FCM Proxy** (local proxy with key rotation and usage tracking) — new Step 3 in the 5-step flow.
|
|
11
|
+
- **Install support for new tools**: Pi (models.json + settings.json), Aider (.aider.conf.yml), Amp (settings.json), Gemini (settings.json), Qwen (modelProviders config), Claude Code/Codex/OpenHands (sourceable env files at ~/.fcm-*-env).
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **Install Endpoints flow is now 5 steps**: Provider → Tool → Connection Mode → Scope → Models (was 4 steps without connection mode choice).
|
|
15
|
+
- **Tool labels in install overlay use metadata**: Tool names and emojis in the Y overlay now come from `tool-metadata.js` instead of hard-coded ternary chains — easier to maintain and always in sync.
|
|
16
|
+
- **Help overlay updated**: Z-cycle hint and CLI flag examples now list all 13 tools.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 0.2.16
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **Changelog index viewport scrolling**: Cursor in changelog version list (N key) now scrolls the viewport to follow selection — previously scrolling past the last visible row would move the cursor off-screen into empty space.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- **Changelog version summaries**: Each version in the changelog index now shows a short summary (first change title, ~15 words) after the change count, displayed in dim for readability.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
5
30
|
## 0.2.15
|
|
6
31
|
|
|
7
32
|
### Changed
|
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ By Vanessa Depraute
|
|
|
87
87
|
- **💻 OpenCode integration** — Auto-detects NIM setup, sets model as default, launches OpenCode
|
|
88
88
|
- **🦞 OpenClaw integration** — Sets selected model as default provider in `~/.openclaw/openclaw.json`
|
|
89
89
|
- **🧰 Public tool launchers** — `Enter` auto-configures and launches 10+ tools: `OpenCode CLI`, `OpenCode Desktop`, `OpenClaw`, `Crush`, `Goose`, `Aider`, `Claude Code`, `Codex`, `Gemini`, `Qwen`, `OpenHands`, `Amp`, and `Pi`. All tools auto-select the chosen model on launch.
|
|
90
|
-
- **🔌 Install Endpoints flow** — Press `Y` to install one configured provider
|
|
90
|
+
- **🔌 Install Endpoints flow** — Press `Y` to install one configured provider into any of the 13 supported tools, with a choice between **Direct Provider** (pure API) or **FCM Proxy** (key rotation + usage tracking), then pick all models or a curated subset
|
|
91
91
|
- **📝 Feature Request (J key)** — Send anonymous feedback directly to the project team
|
|
92
92
|
- **🐛 Bug Report (I key)** — Send anonymous bug reports directly to the project team
|
|
93
93
|
- **🎨 Clean output** — Zero scrollback pollution, interface stays open until Ctrl+C
|
|
@@ -237,8 +237,8 @@ free-coding-models --tier S --json
|
|
|
237
237
|
|
|
238
238
|
Running `free-coding-models` with no launcher flag starts in **OpenCode CLI** mode.
|
|
239
239
|
|
|
240
|
-
- Press **`Z`** in the TUI to cycle the public launch targets: `OpenCode CLI` → `OpenCode Desktop` → `OpenClaw` → `Crush` → `Goose`
|
|
241
|
-
- Or start directly in the target mode with a CLI flag such as `--opencode-desktop`, `--openclaw`, `--crush`, or `--
|
|
240
|
+
- Press **`Z`** in the TUI to cycle the public launch targets: `OpenCode CLI` → `OpenCode Desktop` → `OpenClaw` → `Crush` → `Goose` → `Pi` → `Aider` → `Claude Code` → `Codex` → `Gemini` → `Qwen` → `OpenHands` → `Amp`
|
|
241
|
+
- Or start directly in the target mode with a CLI flag such as `--opencode-desktop`, `--openclaw`, `--crush`, `--goose`, `--pi`, `--aider`, `--claude-code`, `--codex`, `--gemini`, `--qwen`, `--openhands`, or `--amp`
|
|
242
242
|
- The active target is always visible in the header badge before you press `Enter`
|
|
243
243
|
|
|
244
244
|
**How it works:**
|
|
@@ -615,7 +615,9 @@ You can use `free-coding-models` with 12+ AI coding tools. When you select a mod
|
|
|
615
615
|
| **Amp** | `--amp` | ~/.config/amp/settings.json |
|
|
616
616
|
| **Pi** | `--pi` | ~/.pi/agent/settings.json |
|
|
617
617
|
|
|
618
|
-
Press **Z** to cycle through
|
|
618
|
+
Press **Z** to cycle through all 13 tool modes in the TUI, or use flags to start in your preferred mode.
|
|
619
|
+
|
|
620
|
+
All tools are also available as install targets in the **Install Endpoints** flow (`Y` key) — install an entire provider catalog into any tool with one flow, choosing between Direct Provider or FCM Proxy connection.
|
|
619
621
|
|
|
620
622
|
---
|
|
621
623
|
|
|
@@ -924,11 +926,11 @@ This script:
|
|
|
924
926
|
- **T** — Cycle tier filter (All → S+ → S → A+ → A → A- → B+ → B → C → All)
|
|
925
927
|
- **D** — Cycle provider filter (All → NIM → Groq → ...)
|
|
926
928
|
- **E** — Toggle configured-only mode (on by default, persisted across sessions and profiles)
|
|
927
|
-
- **Z** — Cycle target tool (OpenCode CLI →
|
|
929
|
+
- **Z** — Cycle target tool (OpenCode CLI → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Claude Code → Codex → Gemini → Qwen → OpenHands → Amp)
|
|
928
930
|
- **X** — Toggle request logs (recent proxied request/token usage logs, up to 500 entries)
|
|
929
931
|
- **A (in logs)** — Toggle between showing 500 entries or ALL logs
|
|
930
932
|
- **P** — Open Settings (manage API keys, toggles, updates, profiles)
|
|
931
|
-
- **Y** — Open Install Endpoints (`provider → tool →
|
|
933
|
+
- **Y** — Open Install Endpoints (`provider → tool → connection mode → scope → models`, Direct or FCM Proxy)
|
|
932
934
|
- **Shift+P** — Cycle through saved profiles (switches live TUI settings)
|
|
933
935
|
- **Shift+S** — Save current TUI settings as a named profile (inline prompt)
|
|
934
936
|
- **Q** — Open Smart Recommend overlay (find the best model for your task)
|
|
@@ -942,18 +944,25 @@ Pressing **K** now shows a full in-app reference: main hotkeys, settings hotkeys
|
|
|
942
944
|
|
|
943
945
|
### 🔌 Install Endpoints (`Y`)
|
|
944
946
|
|
|
945
|
-
`Y` opens a dedicated install flow for configured providers. The flow is:
|
|
947
|
+
`Y` opens a dedicated install flow for configured providers. The 5-step flow is:
|
|
946
948
|
|
|
947
|
-
1. Pick one provider that already has an API key in Settings
|
|
948
|
-
2. Pick the target tool
|
|
949
|
-
|
|
949
|
+
1. **Provider** — Pick one provider that already has an API key in Settings
|
|
950
|
+
2. **Tool** — Pick the target tool from all 13 supported tools:
|
|
951
|
+
- Config-based: `OpenCode CLI`, `OpenCode Desktop`, `OpenClaw`, `Crush`, `Goose`, `Pi`, `Aider`, `Amp`, `Gemini`, `Qwen`
|
|
952
|
+
- Env-file based: `Claude Code`, `Codex CLI`, `OpenHands` (writes `~/.fcm-{tool}-env` — source it before launching)
|
|
953
|
+
3. **Connection Mode** — Choose how the tool connects to the provider:
|
|
954
|
+
- **⚡ Direct Provider** — pure API connection, no proxy involved
|
|
955
|
+
- **🔄 FCM Proxy** — route through the local FCM proxy with key rotation and usage tracking
|
|
956
|
+
4. **Scope** — Choose `Install all models` or `Install selected models only`
|
|
957
|
+
5. **Models** (if scope = selected) — Multi-select individual models from the provider catalog
|
|
950
958
|
|
|
951
959
|
Important behavior:
|
|
952
960
|
|
|
953
|
-
- Installs are written
|
|
961
|
+
- Installs are written into the target tool config as FCM-managed entries (namespaced under `fcm-*`)
|
|
954
962
|
- `Install all models` is the recommended path because FCM can refresh that catalog automatically on later launches when the provider model list changes
|
|
955
963
|
- `Install selected models only` is useful when you want a smaller curated picker inside the target tool
|
|
956
964
|
- `OpenCode CLI` and `OpenCode Desktop` share the same `opencode.json`, so the managed provider appears in both
|
|
965
|
+
- For env-based tools (Claude Code, Codex, OpenHands), FCM writes a sourceable file at `~/.fcm-{tool}-env` — run `source ~/.fcm-claude-code-env` before launching
|
|
957
966
|
|
|
958
967
|
**Keyboard shortcuts (Settings screen — `P` key):**
|
|
959
968
|
- **↑↓** — Navigate providers, maintenance row, and profile rows
|
|
@@ -118,9 +118,9 @@ import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop, startProxyAn
|
|
|
118
118
|
import { startOpenClaw } from '../src/openclaw.js'
|
|
119
119
|
import { createOverlayRenderers } from '../src/overlays.js'
|
|
120
120
|
import { createKeyHandler } from '../src/key-handler.js'
|
|
121
|
-
import { getToolModeOrder } from '../src/tool-metadata.js'
|
|
121
|
+
import { getToolModeOrder, getToolMeta } from '../src/tool-metadata.js'
|
|
122
122
|
import { startExternalTool } from '../src/tool-launchers.js'
|
|
123
|
-
import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels } from '../src/endpoint-installer.js'
|
|
123
|
+
import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels, CONNECTION_MODES } from '../src/endpoint-installer.js'
|
|
124
124
|
import { loadCache, saveCache, clearCache, getCacheAge } from '../src/cache.js'
|
|
125
125
|
import { checkConfigSecurity } from '../src/security.js'
|
|
126
126
|
|
|
@@ -403,11 +403,12 @@ async function main() {
|
|
|
403
403
|
helpScrollOffset: 0, // 📖 Vertical scroll offset for Help overlay viewport
|
|
404
404
|
// 📖 Install Endpoints overlay state (Y key opens it)
|
|
405
405
|
installEndpointsOpen: false, // 📖 Whether the install-endpoints overlay is active
|
|
406
|
-
installEndpointsPhase: 'providers', // 📖 providers | tools | scope | models | result
|
|
406
|
+
installEndpointsPhase: 'providers', // 📖 providers | tools | connection | scope | models | result
|
|
407
407
|
installEndpointsCursor: 0, // 📖 Selected row within the current install phase
|
|
408
408
|
installEndpointsScrollOffset: 0, // 📖 Vertical scroll offset for the install overlay viewport
|
|
409
409
|
installEndpointsProviderKey: null, // 📖 Selected provider for endpoint installation
|
|
410
410
|
installEndpointsToolMode: null, // 📖 Selected target tool mode
|
|
411
|
+
installEndpointsConnectionMode: null, // 📖 'direct' | 'proxy' — how the tool connects to the provider
|
|
411
412
|
installEndpointsScope: null, // 📖 all | selected
|
|
412
413
|
installEndpointsSelectedModelIds: new Set(), // 📖 Multi-select buffer for the selected-models phase
|
|
413
414
|
installEndpointsErrorMsg: null, // 📖 Temporary validation/error message inside the install flow
|
|
@@ -687,6 +688,8 @@ async function main() {
|
|
|
687
688
|
getConfiguredInstallableProviders,
|
|
688
689
|
getInstallTargetModes,
|
|
689
690
|
getProviderCatalogModels,
|
|
691
|
+
CONNECTION_MODES,
|
|
692
|
+
getToolMeta,
|
|
690
693
|
})
|
|
691
694
|
|
|
692
695
|
onKeyPress = createKeyHandler({
|
|
@@ -711,6 +714,7 @@ async function main() {
|
|
|
711
714
|
getInstallTargetModes,
|
|
712
715
|
getProviderCatalogModels,
|
|
713
716
|
installProviderEndpoints,
|
|
717
|
+
CONNECTION_MODES,
|
|
714
718
|
syncFavoriteFlags,
|
|
715
719
|
toggleFavoriteModel,
|
|
716
720
|
sortResultsWithPinnedFavorites,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.17",
|
|
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",
|
|
@@ -13,11 +13,21 @@
|
|
|
13
13
|
* - it merges into existing config files instead of replacing them
|
|
14
14
|
* - it records successful installs in `~/.free-coding-models.json` so catalogs can be refreshed automatically later
|
|
15
15
|
*
|
|
16
|
+
* 📖 Connection modes:
|
|
17
|
+
* - `direct` — connect the tool straight to the provider API endpoint (no proxy)
|
|
18
|
+
* - `proxy` — route through the local FCM proxy (key rotation + usage tracking)
|
|
19
|
+
*
|
|
16
20
|
* 📖 Tool-specific notes:
|
|
17
21
|
* - OpenCode CLI and OpenCode Desktop share the same `opencode.json`
|
|
18
22
|
* - Crush gets a managed provider block in `crush.json`
|
|
19
23
|
* - Goose gets a declarative custom provider JSON + a matching secret in `secrets.yaml`
|
|
20
24
|
* - OpenClaw gets a managed `models.providers` entry plus matching allowlist rows
|
|
25
|
+
* - Pi gets models.json + settings.json under ~/.pi/agent/
|
|
26
|
+
* - Aider gets ~/.aider.conf.yml with OpenAI-compatible config
|
|
27
|
+
* - Amp gets ~/.config/amp/settings.json
|
|
28
|
+
* - Gemini gets ~/.gemini/settings.json
|
|
29
|
+
* - Qwen gets ~/.qwen/settings.json with modelProviders
|
|
30
|
+
* - Claude Code, Codex, OpenHands get a sourceable env file (~/.fcm-{tool}-env)
|
|
21
31
|
*
|
|
22
32
|
* @functions
|
|
23
33
|
* → `getConfiguredInstallableProviders` — list configured providers that support direct endpoint installs
|
|
@@ -44,7 +54,14 @@ import { ENV_VAR_NAMES, PROVIDER_METADATA } from './provider-metadata.js'
|
|
|
44
54
|
import { getToolMeta } from './tool-metadata.js'
|
|
45
55
|
|
|
46
56
|
const DIRECT_INSTALL_UNSUPPORTED_PROVIDERS = new Set(['replicate', 'zai'])
|
|
47
|
-
|
|
57
|
+
// 📖 All supported install targets — matches TOOL_MODE_ORDER in tool-metadata.js
|
|
58
|
+
const INSTALL_TARGET_MODES = ['opencode', 'opencode-desktop', 'openclaw', 'crush', 'goose', 'pi', 'aider', 'claude-code', 'codex', 'gemini', 'qwen', 'openhands', 'amp']
|
|
59
|
+
|
|
60
|
+
// 📖 Connection modes: direct (pure provider) vs FCM proxy (rotates keys)
|
|
61
|
+
export const CONNECTION_MODES = [
|
|
62
|
+
{ key: 'direct', label: 'Direct Provider', hint: 'Connect the tool straight to the provider API — no proxy involved.' },
|
|
63
|
+
{ key: 'proxy', label: 'FCM Proxy', hint: 'Route through the local FCM proxy with key rotation and usage tracking.' },
|
|
64
|
+
]
|
|
48
65
|
|
|
49
66
|
function getDefaultPaths() {
|
|
50
67
|
const home = homedir()
|
|
@@ -54,6 +71,12 @@ function getDefaultPaths() {
|
|
|
54
71
|
crushConfigPath: join(home, '.config', 'crush', 'crush.json'),
|
|
55
72
|
gooseProvidersDir: join(home, '.config', 'goose', 'custom_providers'),
|
|
56
73
|
gooseSecretsPath: join(home, '.config', 'goose', 'secrets.yaml'),
|
|
74
|
+
piModelsPath: join(home, '.pi', 'agent', 'models.json'),
|
|
75
|
+
piSettingsPath: join(home, '.pi', 'agent', 'settings.json'),
|
|
76
|
+
aiderConfigPath: join(home, '.aider.conf.yml'),
|
|
77
|
+
ampConfigPath: join(home, '.config', 'amp', 'settings.json'),
|
|
78
|
+
geminiConfigPath: join(home, '.gemini', 'settings.json'),
|
|
79
|
+
qwenConfigPath: join(home, '.qwen', 'settings.json'),
|
|
57
80
|
}
|
|
58
81
|
}
|
|
59
82
|
|
|
@@ -374,8 +397,131 @@ function installIntoOpenClaw(providerKey, models, apiKey, paths) {
|
|
|
374
397
|
return { path: filePath, backupPath, providerId, modelCount: models.length }
|
|
375
398
|
}
|
|
376
399
|
|
|
400
|
+
// 📖 installIntoPi writes models.json + settings.json under ~/.pi/agent/
|
|
401
|
+
function installIntoPi(providerKey, models, apiKey, paths) {
|
|
402
|
+
const providerId = getManagedProviderId(providerKey)
|
|
403
|
+
const baseUrl = resolveProviderBaseUrl(providerKey)
|
|
404
|
+
|
|
405
|
+
// 📖 Write models.json with provider config
|
|
406
|
+
const modelsConfig = readJson(paths.piModelsPath, { providers: {} })
|
|
407
|
+
if (!modelsConfig.providers || typeof modelsConfig.providers !== 'object') modelsConfig.providers = {}
|
|
408
|
+
modelsConfig.providers[providerId] = {
|
|
409
|
+
baseUrl,
|
|
410
|
+
api: 'openai-completions',
|
|
411
|
+
apiKey,
|
|
412
|
+
models: models.map((model) => ({ id: model.modelId, name: model.label })),
|
|
413
|
+
}
|
|
414
|
+
const modelsBackupPath = writeJson(paths.piModelsPath, modelsConfig)
|
|
415
|
+
|
|
416
|
+
// 📖 Write settings.json to set default provider
|
|
417
|
+
const settingsConfig = readJson(paths.piSettingsPath, {})
|
|
418
|
+
settingsConfig.defaultProvider = providerId
|
|
419
|
+
settingsConfig.defaultModel = models[0]?.modelId ?? ''
|
|
420
|
+
writeJson(paths.piSettingsPath, settingsConfig, { backup: true })
|
|
421
|
+
|
|
422
|
+
return { path: paths.piModelsPath, backupPath: modelsBackupPath, providerId, modelCount: models.length }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 📖 installIntoAider writes ~/.aider.conf.yml with OpenAI-compatible config
|
|
426
|
+
function installIntoAider(providerKey, models, apiKey, paths) {
|
|
427
|
+
const providerId = getManagedProviderId(providerKey)
|
|
428
|
+
const baseUrl = resolveProviderBaseUrl(providerKey)
|
|
429
|
+
const backupPath = backupIfExists(paths.aiderConfigPath)
|
|
430
|
+
// 📖 Aider YAML config — one model at a time, uses first selected model
|
|
431
|
+
const primaryModel = models[0]
|
|
432
|
+
const lines = [
|
|
433
|
+
'# 📖 Managed by free-coding-models',
|
|
434
|
+
`openai-api-base: ${baseUrl}`,
|
|
435
|
+
`openai-api-key: ${apiKey}`,
|
|
436
|
+
`model: openai/${primaryModel.modelId}`,
|
|
437
|
+
'',
|
|
438
|
+
]
|
|
439
|
+
ensureDirFor(paths.aiderConfigPath)
|
|
440
|
+
writeFileSync(paths.aiderConfigPath, lines.join('\n'))
|
|
441
|
+
return { path: paths.aiderConfigPath, backupPath, providerId, modelCount: models.length }
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 📖 installIntoAmp writes ~/.config/amp/settings.json with model+URL
|
|
445
|
+
function installIntoAmp(providerKey, models, apiKey, paths) {
|
|
446
|
+
const providerId = getManagedProviderId(providerKey)
|
|
447
|
+
const baseUrl = resolveProviderBaseUrl(providerKey)
|
|
448
|
+
const config = readJson(paths.ampConfigPath, {})
|
|
449
|
+
config['amp.url'] = baseUrl
|
|
450
|
+
config['amp.model'] = models[0]?.modelId ?? ''
|
|
451
|
+
const backupPath = writeJson(paths.ampConfigPath, config)
|
|
452
|
+
return { path: paths.ampConfigPath, backupPath, providerId, modelCount: models.length }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 📖 installIntoGemini writes ~/.gemini/settings.json with model ID
|
|
456
|
+
function installIntoGemini(providerKey, models, apiKey, paths) {
|
|
457
|
+
const providerId = getManagedProviderId(providerKey)
|
|
458
|
+
const config = readJson(paths.geminiConfigPath, {})
|
|
459
|
+
config.model = models[0]?.modelId ?? ''
|
|
460
|
+
const backupPath = writeJson(paths.geminiConfigPath, config)
|
|
461
|
+
return { path: paths.geminiConfigPath, backupPath, providerId, modelCount: models.length }
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 📖 installIntoQwen writes ~/.qwen/settings.json with modelProviders config
|
|
465
|
+
function installIntoQwen(providerKey, models, apiKey, paths) {
|
|
466
|
+
const providerId = getManagedProviderId(providerKey)
|
|
467
|
+
const baseUrl = resolveProviderBaseUrl(providerKey)
|
|
468
|
+
const config = readJson(paths.qwenConfigPath, {})
|
|
469
|
+
if (!config.modelProviders || typeof config.modelProviders !== 'object') config.modelProviders = {}
|
|
470
|
+
if (!Array.isArray(config.modelProviders.openai)) config.modelProviders.openai = []
|
|
471
|
+
|
|
472
|
+
// 📖 Remove existing FCM-managed entries, then prepend all selected models
|
|
473
|
+
const filtered = config.modelProviders.openai.filter(
|
|
474
|
+
(entry) => !models.some((m) => m.modelId === entry?.id)
|
|
475
|
+
)
|
|
476
|
+
const newEntries = models.map((model) => ({
|
|
477
|
+
id: model.modelId,
|
|
478
|
+
name: model.label,
|
|
479
|
+
envKey: ENV_VAR_NAMES[providerKey] || 'OPENAI_API_KEY',
|
|
480
|
+
baseUrl,
|
|
481
|
+
}))
|
|
482
|
+
config.modelProviders.openai = [...newEntries, ...filtered]
|
|
483
|
+
config.model = models[0]?.modelId ?? ''
|
|
484
|
+
const backupPath = writeJson(paths.qwenConfigPath, config)
|
|
485
|
+
return { path: paths.qwenConfigPath, backupPath, providerId, modelCount: models.length }
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 📖 installIntoEnvBasedTool handles tools that rely on env vars only (claude-code, codex, openhands).
|
|
489
|
+
// 📖 We write a small .env-style helper file so users can source it before launching.
|
|
490
|
+
function installIntoEnvBasedTool(providerKey, models, apiKey, toolMode, paths) {
|
|
491
|
+
const providerId = getManagedProviderId(providerKey)
|
|
492
|
+
const baseUrl = resolveProviderBaseUrl(providerKey)
|
|
493
|
+
const home = homedir()
|
|
494
|
+
const envFileName = `.fcm-${toolMode}-env`
|
|
495
|
+
const envFilePath = join(home, envFileName)
|
|
496
|
+
|
|
497
|
+
const primaryModel = models[0]
|
|
498
|
+
const envLines = [
|
|
499
|
+
'# 📖 Managed by free-coding-models — source this file before launching the tool',
|
|
500
|
+
`# 📖 Provider: ${getProviderLabel(providerKey)} (${models.length} models)`,
|
|
501
|
+
`export OPENAI_API_KEY="${apiKey}"`,
|
|
502
|
+
`export OPENAI_BASE_URL="${baseUrl}"`,
|
|
503
|
+
`export OPENAI_MODEL="${primaryModel.modelId}"`,
|
|
504
|
+
`export LLM_API_KEY="${apiKey}"`,
|
|
505
|
+
`export LLM_BASE_URL="${baseUrl}"`,
|
|
506
|
+
`export LLM_MODEL="openai/${primaryModel.modelId}"`,
|
|
507
|
+
]
|
|
508
|
+
|
|
509
|
+
// 📖 Tool-specific extra env vars
|
|
510
|
+
if (toolMode === 'claude-code') {
|
|
511
|
+
envLines.push(`export ANTHROPIC_AUTH_TOKEN="${apiKey}"`)
|
|
512
|
+
envLines.push(`export ANTHROPIC_BASE_URL="${baseUrl}"`)
|
|
513
|
+
envLines.push(`export ANTHROPIC_MODEL="${primaryModel.modelId}"`)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
ensureDirFor(envFilePath)
|
|
517
|
+
const backupPath = backupIfExists(envFilePath)
|
|
518
|
+
writeFileSync(envFilePath, envLines.join('\n') + '\n')
|
|
519
|
+
return { path: envFilePath, backupPath, providerId, modelCount: models.length }
|
|
520
|
+
}
|
|
521
|
+
|
|
377
522
|
export function installProviderEndpoints(config, providerKey, toolMode, options = {}) {
|
|
378
523
|
const canonicalToolMode = canonicalizeToolMode(toolMode)
|
|
524
|
+
const connectionMode = options.connectionMode || 'direct'
|
|
379
525
|
const support = getDirectInstallSupport(providerKey)
|
|
380
526
|
if (!support.supported) {
|
|
381
527
|
throw new Error(support.reason || 'Direct install is not supported for this provider')
|
|
@@ -389,6 +535,7 @@ export function installProviderEndpoints(config, providerKey, toolMode, options
|
|
|
389
535
|
}
|
|
390
536
|
|
|
391
537
|
const paths = { ...getDefaultPaths(), ...(options.paths || {}) }
|
|
538
|
+
// 📖 Dispatch to the right installer based on canonical tool mode
|
|
392
539
|
let installResult
|
|
393
540
|
if (canonicalToolMode === 'opencode') {
|
|
394
541
|
installResult = installIntoOpenCode(providerKey, models, apiKey, paths)
|
|
@@ -398,6 +545,18 @@ export function installProviderEndpoints(config, providerKey, toolMode, options
|
|
|
398
545
|
installResult = installIntoCrush(providerKey, models, apiKey, paths)
|
|
399
546
|
} else if (canonicalToolMode === 'goose') {
|
|
400
547
|
installResult = installIntoGoose(providerKey, models, apiKey, paths)
|
|
548
|
+
} else if (canonicalToolMode === 'pi') {
|
|
549
|
+
installResult = installIntoPi(providerKey, models, apiKey, paths)
|
|
550
|
+
} else if (canonicalToolMode === 'aider') {
|
|
551
|
+
installResult = installIntoAider(providerKey, models, apiKey, paths)
|
|
552
|
+
} else if (canonicalToolMode === 'amp') {
|
|
553
|
+
installResult = installIntoAmp(providerKey, models, apiKey, paths)
|
|
554
|
+
} else if (canonicalToolMode === 'gemini') {
|
|
555
|
+
installResult = installIntoGemini(providerKey, models, apiKey, paths)
|
|
556
|
+
} else if (canonicalToolMode === 'qwen') {
|
|
557
|
+
installResult = installIntoQwen(providerKey, models, apiKey, paths)
|
|
558
|
+
} else if (canonicalToolMode === 'claude-code' || canonicalToolMode === 'codex' || canonicalToolMode === 'openhands') {
|
|
559
|
+
installResult = installIntoEnvBasedTool(providerKey, models, apiKey, canonicalToolMode, paths)
|
|
401
560
|
} else {
|
|
402
561
|
throw new Error(`Unsupported install target: ${toolMode}`)
|
|
403
562
|
}
|
|
@@ -414,6 +573,7 @@ export function installProviderEndpoints(config, providerKey, toolMode, options
|
|
|
414
573
|
providerKey,
|
|
415
574
|
providerLabel: getProviderLabel(providerKey),
|
|
416
575
|
scope,
|
|
576
|
+
connectionMode,
|
|
417
577
|
autoRefreshEnabled: true,
|
|
418
578
|
models,
|
|
419
579
|
}
|
package/src/key-handler.js
CHANGED
|
@@ -169,6 +169,7 @@ export function createKeyHandler(ctx) {
|
|
|
169
169
|
getInstallTargetModes,
|
|
170
170
|
getProviderCatalogModels,
|
|
171
171
|
installProviderEndpoints,
|
|
172
|
+
CONNECTION_MODES,
|
|
172
173
|
syncFavoriteFlags,
|
|
173
174
|
toggleFavoriteModel,
|
|
174
175
|
sortResultsWithPinnedFavorites,
|
|
@@ -334,6 +335,7 @@ export function createKeyHandler(ctx) {
|
|
|
334
335
|
state.installEndpointsScrollOffset = 0
|
|
335
336
|
state.installEndpointsProviderKey = null
|
|
336
337
|
state.installEndpointsToolMode = null
|
|
338
|
+
state.installEndpointsConnectionMode = null
|
|
337
339
|
state.installEndpointsScope = null
|
|
338
340
|
state.installEndpointsSelectedModelIds = new Set()
|
|
339
341
|
state.installEndpointsErrorMsg = null
|
|
@@ -349,6 +351,7 @@ export function createKeyHandler(ctx) {
|
|
|
349
351
|
{
|
|
350
352
|
scope: state.installEndpointsScope,
|
|
351
353
|
modelIds: selectedModelIds,
|
|
354
|
+
connectionMode: state.installEndpointsConnectionMode || 'direct',
|
|
352
355
|
}
|
|
353
356
|
)
|
|
354
357
|
|
|
@@ -414,12 +417,13 @@ export function createKeyHandler(ctx) {
|
|
|
414
417
|
return
|
|
415
418
|
}
|
|
416
419
|
|
|
417
|
-
// 📖 Install Endpoints overlay: provider → tool → scope → optional model subset.
|
|
420
|
+
// 📖 Install Endpoints overlay: provider → tool → connection → scope → optional model subset.
|
|
418
421
|
if (state.installEndpointsOpen) {
|
|
419
422
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
420
423
|
|
|
421
424
|
const providerChoices = getConfiguredInstallableProviders(state.config)
|
|
422
425
|
const toolChoices = getInstallTargetModes()
|
|
426
|
+
const connectionChoices = CONNECTION_MODES || []
|
|
423
427
|
const modelChoices = state.installEndpointsProviderKey
|
|
424
428
|
? getProviderCatalogModels(state.installEndpointsProviderKey)
|
|
425
429
|
: []
|
|
@@ -428,6 +432,7 @@ export function createKeyHandler(ctx) {
|
|
|
428
432
|
const maxIndexByPhase = () => {
|
|
429
433
|
if (state.installEndpointsPhase === 'providers') return Math.max(0, providerChoices.length - 1)
|
|
430
434
|
if (state.installEndpointsPhase === 'tools') return Math.max(0, toolChoices.length - 1)
|
|
435
|
+
if (state.installEndpointsPhase === 'connection') return Math.max(0, connectionChoices.length - 1)
|
|
431
436
|
if (state.installEndpointsPhase === 'scope') return 1
|
|
432
437
|
if (state.installEndpointsPhase === 'models') return Math.max(0, modelChoices.length - 1)
|
|
433
438
|
return 0
|
|
@@ -470,12 +475,18 @@ export function createKeyHandler(ctx) {
|
|
|
470
475
|
state.installEndpointsScrollOffset = 0
|
|
471
476
|
return
|
|
472
477
|
}
|
|
473
|
-
if (state.installEndpointsPhase === '
|
|
478
|
+
if (state.installEndpointsPhase === 'connection') {
|
|
474
479
|
state.installEndpointsPhase = 'tools'
|
|
475
480
|
state.installEndpointsCursor = 0
|
|
476
481
|
state.installEndpointsScrollOffset = 0
|
|
477
482
|
return
|
|
478
483
|
}
|
|
484
|
+
if (state.installEndpointsPhase === 'scope') {
|
|
485
|
+
state.installEndpointsPhase = 'connection'
|
|
486
|
+
state.installEndpointsCursor = state.installEndpointsConnectionMode === 'proxy' ? 1 : 0
|
|
487
|
+
state.installEndpointsScrollOffset = 0
|
|
488
|
+
return
|
|
489
|
+
}
|
|
479
490
|
if (state.installEndpointsPhase === 'models') {
|
|
480
491
|
state.installEndpointsPhase = 'scope'
|
|
481
492
|
state.installEndpointsCursor = state.installEndpointsScope === 'selected' ? 1 : 0
|
|
@@ -508,6 +519,20 @@ export function createKeyHandler(ctx) {
|
|
|
508
519
|
const selectedToolMode = toolChoices[state.installEndpointsCursor]
|
|
509
520
|
if (!selectedToolMode) return
|
|
510
521
|
state.installEndpointsToolMode = selectedToolMode
|
|
522
|
+
state.installEndpointsPhase = 'connection'
|
|
523
|
+
state.installEndpointsCursor = 0
|
|
524
|
+
state.installEndpointsScrollOffset = 0
|
|
525
|
+
state.installEndpointsErrorMsg = null
|
|
526
|
+
}
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// 📖 Connection mode phase: Direct Provider vs FCM Proxy
|
|
531
|
+
if (state.installEndpointsPhase === 'connection') {
|
|
532
|
+
if (key.name === 'return') {
|
|
533
|
+
const selected = connectionChoices[state.installEndpointsCursor]
|
|
534
|
+
if (!selected) return
|
|
535
|
+
state.installEndpointsConnectionMode = selected.key
|
|
511
536
|
state.installEndpointsPhase = 'scope'
|
|
512
537
|
state.installEndpointsCursor = 0
|
|
513
538
|
state.installEndpointsScrollOffset = 0
|
|
@@ -1395,6 +1420,7 @@ export function createKeyHandler(ctx) {
|
|
|
1395
1420
|
state.installEndpointsScrollOffset = 0
|
|
1396
1421
|
state.installEndpointsProviderKey = null
|
|
1397
1422
|
state.installEndpointsToolMode = null
|
|
1423
|
+
state.installEndpointsConnectionMode = null
|
|
1398
1424
|
state.installEndpointsScope = null
|
|
1399
1425
|
state.installEndpointsSelectedModelIds = new Set()
|
|
1400
1426
|
state.installEndpointsErrorMsg = null
|
package/src/overlays.js
CHANGED
|
@@ -55,6 +55,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
55
55
|
getConfiguredInstallableProviders,
|
|
56
56
|
getInstallTargetModes,
|
|
57
57
|
getProviderCatalogModels,
|
|
58
|
+
CONNECTION_MODES,
|
|
59
|
+
getToolMeta,
|
|
58
60
|
} = deps
|
|
59
61
|
|
|
60
62
|
// 📖 Wrap plain diagnostic text so long Settings messages stay readable inside
|
|
@@ -377,7 +379,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
377
379
|
}
|
|
378
380
|
|
|
379
381
|
// ─── Install Endpoints overlay renderer ───────────────────────────────────
|
|
380
|
-
// 📖 renderInstallEndpoints drives the provider → tool → scope → model flow
|
|
382
|
+
// 📖 renderInstallEndpoints drives the provider → tool → connection → scope → model flow
|
|
381
383
|
// 📖 behind the `Y` hotkey. It deliberately reuses the same overlay viewport
|
|
382
384
|
// 📖 helpers as Settings so long provider/model lists stay navigable.
|
|
383
385
|
function renderInstallEndpoints() {
|
|
@@ -386,6 +388,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
386
388
|
const cursorLineByRow = {}
|
|
387
389
|
const providerChoices = getConfiguredInstallableProviders(state.config)
|
|
388
390
|
const toolChoices = getInstallTargetModes()
|
|
391
|
+
const connectionChoices = CONNECTION_MODES || []
|
|
392
|
+
const totalSteps = 5
|
|
389
393
|
const scopeChoices = [
|
|
390
394
|
{
|
|
391
395
|
key: 'all',
|
|
@@ -401,18 +405,22 @@ export function createOverlayRenderers(state, deps) {
|
|
|
401
405
|
const selectedProviderLabel = state.installEndpointsProviderKey
|
|
402
406
|
? (sources[state.installEndpointsProviderKey]?.name || state.installEndpointsProviderKey)
|
|
403
407
|
: '—'
|
|
408
|
+
|
|
409
|
+
// 📖 Resolve tool label from metadata instead of hard-coded switch
|
|
404
410
|
const selectedToolLabel = state.installEndpointsToolMode
|
|
405
|
-
? (
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
? 'OpenClaw'
|
|
411
|
-
: state.installEndpointsToolMode === 'crush'
|
|
412
|
-
? 'Crush'
|
|
413
|
-
: 'Goose'))
|
|
411
|
+
? (() => {
|
|
412
|
+
const meta = getToolMeta(state.installEndpointsToolMode)
|
|
413
|
+
const suffix = state.installEndpointsToolMode.startsWith('opencode') ? ' (shared opencode.json)' : ''
|
|
414
|
+
return `${meta.label}${suffix}`
|
|
415
|
+
})()
|
|
414
416
|
: '—'
|
|
415
417
|
|
|
418
|
+
const selectedConnectionLabel = state.installEndpointsConnectionMode === 'proxy'
|
|
419
|
+
? 'FCM Proxy'
|
|
420
|
+
: state.installEndpointsConnectionMode === 'direct'
|
|
421
|
+
? 'Direct Provider'
|
|
422
|
+
: '—'
|
|
423
|
+
|
|
416
424
|
lines.push('')
|
|
417
425
|
// 📖 Branding header
|
|
418
426
|
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
|
|
@@ -425,7 +433,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
425
433
|
lines.push('')
|
|
426
434
|
|
|
427
435
|
if (state.installEndpointsPhase === 'providers') {
|
|
428
|
-
lines.push(` ${chalk.bold(
|
|
436
|
+
lines.push(` ${chalk.bold(`Step 1/${totalSteps}`)} ${chalk.cyan('Choose a configured provider')}`)
|
|
429
437
|
lines.push('')
|
|
430
438
|
|
|
431
439
|
if (providerChoices.length === 0) {
|
|
@@ -435,7 +443,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
435
443
|
providerChoices.forEach((provider, idx) => {
|
|
436
444
|
const isCursor = idx === state.installEndpointsCursor
|
|
437
445
|
const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
438
|
-
const row = `${bullet}${chalk.bold(provider.label.padEnd(24))} ${chalk.dim(`${provider.modelCount} models`)}`
|
|
446
|
+
const row = `${bullet}${chalk.bold(provider.label.padEnd(24))} ${chalk.dim(`${provider.modelCount} models`)}`
|
|
439
447
|
cursorLineByRow[idx] = lines.length
|
|
440
448
|
lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
|
|
441
449
|
})
|
|
@@ -444,37 +452,51 @@ export function createOverlayRenderers(state, deps) {
|
|
|
444
452
|
lines.push('')
|
|
445
453
|
lines.push(chalk.dim(' ↑↓ Navigate • Enter Choose provider • Esc Close'))
|
|
446
454
|
} else if (state.installEndpointsPhase === 'tools') {
|
|
447
|
-
lines.push(` ${chalk.bold(
|
|
455
|
+
lines.push(` ${chalk.bold(`Step 2/${totalSteps}`)} ${chalk.cyan('Choose the target tool')}`)
|
|
448
456
|
lines.push(chalk.dim(` Provider: ${selectedProviderLabel}`))
|
|
449
457
|
lines.push('')
|
|
450
458
|
|
|
459
|
+
// 📖 Use getToolMeta for labels instead of hard-coded ternary chains
|
|
451
460
|
toolChoices.forEach((toolMode, idx) => {
|
|
452
461
|
const isCursor = idx === state.installEndpointsCursor
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
: toolMode === 'opencode'
|
|
456
|
-
? 'OpenCode CLI'
|
|
457
|
-
: toolMode === 'openclaw'
|
|
458
|
-
? 'OpenClaw'
|
|
459
|
-
: toolMode === 'crush'
|
|
460
|
-
? 'Crush'
|
|
461
|
-
: 'Goose'
|
|
462
|
+
const meta = getToolMeta(toolMode)
|
|
463
|
+
const label = `${meta.emoji} ${meta.label}`
|
|
462
464
|
const note = toolMode.startsWith('opencode')
|
|
463
465
|
? chalk.dim('shared config file')
|
|
464
|
-
:
|
|
466
|
+
: ['claude-code', 'codex', 'openhands'].includes(toolMode)
|
|
467
|
+
? chalk.dim('env file (~/.fcm-*-env)')
|
|
468
|
+
: chalk.dim('managed config install')
|
|
465
469
|
const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
466
|
-
const row = `${bullet}${chalk.bold(label.padEnd(
|
|
470
|
+
const row = `${bullet}${chalk.bold(label.padEnd(26))} ${note}`
|
|
467
471
|
cursorLineByRow[idx] = lines.length
|
|
468
472
|
lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
|
|
469
473
|
})
|
|
470
474
|
|
|
471
475
|
lines.push('')
|
|
472
476
|
lines.push(chalk.dim(' ↑↓ Navigate • Enter Choose tool • Esc Back'))
|
|
473
|
-
} else if (state.installEndpointsPhase === '
|
|
474
|
-
|
|
477
|
+
} else if (state.installEndpointsPhase === 'connection') {
|
|
478
|
+
// 📖 Step 3: Choose connection mode — Direct Provider vs FCM Proxy
|
|
479
|
+
lines.push(` ${chalk.bold(`Step 3/${totalSteps}`)} ${chalk.cyan('Choose connection mode')}`)
|
|
475
480
|
lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel}`))
|
|
476
481
|
lines.push('')
|
|
477
482
|
|
|
483
|
+
connectionChoices.forEach((mode, idx) => {
|
|
484
|
+
const isCursor = idx === state.installEndpointsCursor
|
|
485
|
+
const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
486
|
+
const icon = mode.key === 'proxy' ? '🔄' : '⚡'
|
|
487
|
+
const row = `${bullet}${icon} ${chalk.bold(mode.label)}`
|
|
488
|
+
cursorLineByRow[idx] = lines.length
|
|
489
|
+
lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
|
|
490
|
+
lines.push(chalk.dim(` ${mode.hint}`))
|
|
491
|
+
lines.push('')
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
lines.push(chalk.dim(' Enter Continue • Esc Back'))
|
|
495
|
+
} else if (state.installEndpointsPhase === 'scope') {
|
|
496
|
+
lines.push(` ${chalk.bold(`Step 4/${totalSteps}`)} ${chalk.cyan('Choose the install scope')}`)
|
|
497
|
+
lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel} • ${selectedConnectionLabel}`))
|
|
498
|
+
lines.push('')
|
|
499
|
+
|
|
478
500
|
scopeChoices.forEach((scope, idx) => {
|
|
479
501
|
const isCursor = idx === state.installEndpointsCursor
|
|
480
502
|
const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
@@ -490,8 +512,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
490
512
|
const models = getProviderCatalogModels(state.installEndpointsProviderKey)
|
|
491
513
|
const selectedCount = state.installEndpointsSelectedModelIds.size
|
|
492
514
|
|
|
493
|
-
lines.push(` ${chalk.bold(
|
|
494
|
-
lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel}`))
|
|
515
|
+
lines.push(` ${chalk.bold(`Step 5/${totalSteps}`)} ${chalk.cyan('Choose which models to install')}`)
|
|
516
|
+
lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel} • ${selectedConnectionLabel}`))
|
|
495
517
|
lines.push(chalk.dim(` Selected: ${selectedCount}/${models.length}`))
|
|
496
518
|
lines.push('')
|
|
497
519
|
|
|
@@ -608,9 +630,9 @@ export function createOverlayRenderers(state, deps) {
|
|
|
608
630
|
lines.push(` ${chalk.yellow('W')} Toggle ping mode ${chalk.dim('(speed 2s → normal 10s → slow 30s → forced 4s)')}`)
|
|
609
631
|
lines.push(` ${chalk.yellow('E')} Toggle configured models only ${chalk.dim('(enabled by default, persisted globally + in profiles)')}`)
|
|
610
632
|
lines.push(` ${chalk.yellow('X')} Toggle token log page ${chalk.dim('(shows recent request usage from request-log.jsonl)')}`)
|
|
611
|
-
lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode → Desktop → OpenClaw → Crush → Goose)')}`)
|
|
633
|
+
lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Claude Code → Codex → Gemini → Qwen → OpenHands → Amp)')}`)
|
|
612
634
|
lines.push(` ${chalk.yellow('F')} Toggle favorite on selected row ${chalk.dim('(⭐ pinned at top, persisted)')}`)
|
|
613
|
-
lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog →
|
|
635
|
+
lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog → all tools, Direct or FCM Proxy)')}`)
|
|
614
636
|
lines.push(` ${chalk.yellow('Q')} Smart Recommend ${chalk.dim('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
|
|
615
637
|
lines.push(` ${chalk.rgb(57, 255, 20).bold('J')} Request Feature ${chalk.dim('(📝 send anonymous feedback to the project team)')}`)
|
|
616
638
|
lines.push(` ${chalk.rgb(255, 87, 51).bold('I')} Report Bug ${chalk.dim('(🐛 send anonymous bug report to the project team)')}`)
|
|
@@ -641,15 +663,14 @@ export function createOverlayRenderers(state, deps) {
|
|
|
641
663
|
lines.push(` ${chalk.cyan('free-coding-models --openclaw')} ${chalk.dim('OpenClaw mode')}`)
|
|
642
664
|
lines.push(` ${chalk.cyan('free-coding-models --crush')} ${chalk.dim('Crush mode')}`)
|
|
643
665
|
lines.push(` ${chalk.cyan('free-coding-models --goose')} ${chalk.dim('Goose mode')}`)
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
// lines.push(` ${chalk.cyan('free-coding-models --pi')} ${chalk.dim('Pi mode')}`)
|
|
666
|
+
lines.push(` ${chalk.cyan('free-coding-models --pi')} ${chalk.dim('Pi mode')}`)
|
|
667
|
+
lines.push(` ${chalk.cyan('free-coding-models --aider')} ${chalk.dim('Aider mode')}`)
|
|
668
|
+
lines.push(` ${chalk.cyan('free-coding-models --claude-code')} ${chalk.dim('Claude Code mode')}`)
|
|
669
|
+
lines.push(` ${chalk.cyan('free-coding-models --codex')} ${chalk.dim('Codex CLI mode')}`)
|
|
670
|
+
lines.push(` ${chalk.cyan('free-coding-models --gemini')} ${chalk.dim('Gemini CLI mode')}`)
|
|
671
|
+
lines.push(` ${chalk.cyan('free-coding-models --qwen')} ${chalk.dim('Qwen Code mode')}`)
|
|
672
|
+
lines.push(` ${chalk.cyan('free-coding-models --openhands')} ${chalk.dim('OpenHands mode')}`)
|
|
673
|
+
lines.push(` ${chalk.cyan('free-coding-models --amp')} ${chalk.dim('Amp mode')}`)
|
|
653
674
|
lines.push(` ${chalk.cyan('free-coding-models --best')} ${chalk.dim('Only top tiers (A+, S, S+)')}`)
|
|
654
675
|
lines.push(` ${chalk.cyan('free-coding-models --fiable')} ${chalk.dim('10s reliability analysis')}`)
|
|
655
676
|
lines.push(` ${chalk.cyan('free-coding-models --tier S|A|B|C')} ${chalk.dim('Filter by tier letter')}`)
|
|
@@ -1290,12 +1311,32 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1290
1311
|
if (changes[key]) itemCount += changes[key].length
|
|
1291
1312
|
}
|
|
1292
1313
|
|
|
1293
|
-
// 📖
|
|
1294
|
-
const
|
|
1314
|
+
// 📖 Build a short summary from the first few items (max ~15 words, stripped of markdown)
|
|
1315
|
+
const allItems = []
|
|
1316
|
+
for (const k of ['added', 'fixed', 'changed', 'updated']) {
|
|
1317
|
+
if (changes[k]) for (const item of changes[k]) allItems.push(item)
|
|
1318
|
+
}
|
|
1319
|
+
let summary = ''
|
|
1320
|
+
if (allItems.length > 0) {
|
|
1321
|
+
// 📖 Extract the bold title part if present, otherwise use the raw text
|
|
1322
|
+
const firstItem = allItems[0]
|
|
1323
|
+
const boldMatch = firstItem.match(/\*\*([^*]+)\*\*/)
|
|
1324
|
+
const rawText = boldMatch ? boldMatch[1] : firstItem.replace(/\*\*([^*]+)\*\*/g, '$1').replace(/`([^`]+)`/g, '$1')
|
|
1325
|
+
// 📖 Truncate to ~15 words max
|
|
1326
|
+
const words = rawText.split(/\s+/).slice(0, 15)
|
|
1327
|
+
summary = words.join(' ')
|
|
1328
|
+
if (rawText.split(/\s+/).length > 15) summary += '…'
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// 📖 Format version line with selection highlight + dim summary
|
|
1332
|
+
const countStr = `${itemCount} ${itemCount === 1 ? 'change' : 'changes'}`
|
|
1333
|
+
const prefix = ` v${version.padEnd(8)} — ${countStr}`
|
|
1295
1334
|
if (isSelected) {
|
|
1296
|
-
|
|
1335
|
+
const full = summary ? `${prefix} · ${summary}` : prefix
|
|
1336
|
+
lines.push(chalk.inverse(full))
|
|
1297
1337
|
} else {
|
|
1298
|
-
|
|
1338
|
+
const dimSummary = summary ? chalk.dim(` · ${summary}`) : ''
|
|
1339
|
+
lines.push(`${prefix}${dimSummary}`)
|
|
1299
1340
|
}
|
|
1300
1341
|
}
|
|
1301
1342
|
|
|
@@ -1332,6 +1373,17 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1332
1373
|
}
|
|
1333
1374
|
}
|
|
1334
1375
|
|
|
1376
|
+
// 📖 Keep selected changelog row visible by scrolling the overlay viewport (index phase)
|
|
1377
|
+
if (state.changelogPhase === 'index') {
|
|
1378
|
+
const targetLine = 4 + state.changelogCursor // 📖 3 header lines + 1 blank = versions start at line 4
|
|
1379
|
+
state.changelogScrollOffset = keepOverlayTargetVisible(
|
|
1380
|
+
state.changelogScrollOffset,
|
|
1381
|
+
targetLine,
|
|
1382
|
+
lines.length,
|
|
1383
|
+
state.terminalRows
|
|
1384
|
+
)
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1335
1387
|
// 📖 Use scrolling with overlay handler
|
|
1336
1388
|
const CHANGELOG_OVERLAY_BG = chalk.bgRgb(10, 40, 80) // Dark blue background
|
|
1337
1389
|
const { visible, offset } = sliceOverlayLines(lines, state.changelogScrollOffset, state.terminalRows)
|
package/src/tool-metadata.js
CHANGED
|
@@ -29,13 +29,13 @@ export const TOOL_METADATA = {
|
|
|
29
29
|
crush: { label: 'Crush', emoji: '💘', flag: '--crush' },
|
|
30
30
|
goose: { label: 'Goose', emoji: '🪿', flag: '--goose' },
|
|
31
31
|
pi: { label: 'Pi', emoji: 'π', flag: '--pi' },
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
aider: { label: 'Aider', emoji: '🛠', flag: '--aider' },
|
|
33
|
+
'claude-code': { label: 'Claude Code', emoji: '🧠', flag: '--claude-code' },
|
|
34
|
+
codex: { label: 'Codex CLI', emoji: '⌘', flag: '--codex' },
|
|
35
|
+
gemini: { label: 'Gemini CLI', emoji: '✦', flag: '--gemini' },
|
|
36
|
+
qwen: { label: 'Qwen Code', emoji: '🌊', flag: '--qwen' },
|
|
37
|
+
openhands: { label: 'OpenHands', emoji: '🤲', flag: '--openhands' },
|
|
38
|
+
amp: { label: 'Amp', emoji: '⚡', flag: '--amp' },
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export const TOOL_MODE_ORDER = [
|
|
@@ -45,13 +45,13 @@ export const TOOL_MODE_ORDER = [
|
|
|
45
45
|
'crush',
|
|
46
46
|
'goose',
|
|
47
47
|
'pi',
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
'aider',
|
|
49
|
+
'claude-code',
|
|
50
|
+
'codex',
|
|
51
|
+
'gemini',
|
|
52
|
+
'qwen',
|
|
53
|
+
'openhands',
|
|
54
|
+
'amp',
|
|
55
55
|
]
|
|
56
56
|
|
|
57
57
|
export function getToolMeta(mode) {
|