free-coding-models 0.3.42 → 0.3.44
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 +6 -8
- package/README.md +95 -0
- package/package.json +1 -1
- package/src/app.js +11 -2
- package/src/command-palette.js +1 -0
- package/src/config.js +3 -1
- package/src/key-handler.js +105 -1
- package/src/openclaw.js +25 -3
- package/src/opencode.js +28 -0
- package/src/render-table.js +32 -24
- package/src/telemetry.js +32 -13
- package/src/tool-bootstrap.js +14 -0
- package/src/tool-launchers.js +21 -3
- package/src/tool-metadata.js +4 -1
- package/src/utils.js +4 -2
- package/web/dist/assets/{index-fk7MxoC4.js → index-baoFTSG_.js} +1 -1
- package/web/dist/index.html +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
---
|
|
3
|
-
|
|
4
|
-
## [0.3.42] - 2026-04-09
|
|
1
|
+
## [0.3.44] - 2026-04-10
|
|
5
2
|
|
|
6
3
|
### Added
|
|
7
|
-
- Added a
|
|
4
|
+
- Added **jcode** external tool support, integrating a new CLI tool across the codebase with command-palette description, tool metadata (label, emoji, flag, color), bootstrap metadata (binary, docs URL, install commands), and full launch wiring via `prepareExternalToolLaunch` and `startExternalTool`. CLI flag parsing for `--jcode` (jcodeMode) is also included.
|
|
5
|
+
|
|
6
|
+
### Changed
|
|
7
|
+
- Provider API keys are now synced into tool launches — `openclaw` imports `getApiKey` and `syncShellEnv`, accepts an env override in `spawnOpenClawCli`, and populates the child process env with the provider API key when launching the CLI. Tool launchers now use `model.providerKey` instead of hardcoded provider IDs, and `prepareExternalToolLaunch` includes provider and API key in launch args. This enables multi-provider support and ensures launched tools can authenticate using the configured key.
|
|
8
8
|
|
|
9
9
|
### Fixed
|
|
10
|
-
-
|
|
11
|
-
- `free-coding-models --web` now checks whether port `3333` already belongs to another app and automatically starts on the next free local port instead of sending users to a misleading `Not Found` page.
|
|
12
|
-
- npm releases now build the web dashboard during `prepack`, so published installs include `web/dist` and the web UI actually loads after install instead of starting a server with no bundled frontend.
|
|
10
|
+
- README now includes a "Bonus Free Stuff" section with curated resources: community awesome-lists, AI-powered IDEs with free tiers, API providers with permanent free tiers, trial credit providers, and education/developer program freebies — accessible via a new navigation link.
|
package/README.md
CHANGED
|
@@ -34,11 +34,13 @@ create a free account on one of the [providers](#-list-of-free-ai-providers)
|
|
|
34
34
|
<a href="#-why-this-tool">💡 Why</a> •
|
|
35
35
|
<a href="#-quick-start">⚡ Quick Start</a> •
|
|
36
36
|
<a href="#-list-of-free-ai-providers">🟢 Providers</a> •
|
|
37
|
+
<a href="#-bonus-free-stuff">🎁 Bonus Free Stuff</a> •
|
|
37
38
|
<a href="#-usage">🚀 Usage</a> •
|
|
38
39
|
<a href="#-tui-keys">⌨️ TUI Keys</a> •
|
|
39
40
|
<a href="#-features">✨ Features</a> •
|
|
40
41
|
<a href="#-contributing">📋 Contributing</a> •
|
|
41
42
|
<a href="#️-model-licensing--commercial-use">⚖️ Licensing</a> •
|
|
43
|
+
<a href="#-telemetry">📊 Telemetry</a> •
|
|
42
44
|
<a href="#️-security--trust">🛡️ Security</a> •
|
|
43
45
|
<a href="#-support">📧 Support</a> •
|
|
44
46
|
<a href="#-license">📄 License</a>
|
|
@@ -102,6 +104,84 @@ Create a free account on one provider below to get started:
|
|
|
102
104
|
|
|
103
105
|
> 💡 One key is enough. Add more at any time with **`P`** inside the TUI.
|
|
104
106
|
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 🎁 Bonus Free Stuff
|
|
110
|
+
|
|
111
|
+
**Everything free that isn't in the CLI** — IDE extensions, coding agents, GitHub lists, trial credits, and more.
|
|
112
|
+
|
|
113
|
+
### 📚 Awesome Lists (curated by the community)
|
|
114
|
+
|
|
115
|
+
| Resource | What it is |
|
|
116
|
+
|----------|------------|
|
|
117
|
+
| [cheahjs/free-llm-api-resources](https://github.com/cheahjs/free-llm-api-resources) (18.4k ⭐) | Comprehensive list of free LLM API providers with rate limits |
|
|
118
|
+
| [mnfst/awesome-free-llm-apis](https://github.com/mnfst/awesome-free-llm-apis) (2.1k ⭐) | Permanent free LLM API tiers organized by provider |
|
|
119
|
+
| [inmve/free-ai-coding](https://github.com/inmve/free-ai-coding) (648 ⭐) | Pro-grade AI coding tools side-by-side — limits, models, CC requirements |
|
|
120
|
+
| [amardeeplakshkar/awesome-free-llm-apis](https://github.com/amardeeplakshkar/awesome-free-llm-apis) | Additional free LLM API resources |
|
|
121
|
+
|
|
122
|
+
### 🖥️ AI-Powered IDEs with Free Tiers
|
|
123
|
+
|
|
124
|
+
| IDE | Free tier | Credit card |
|
|
125
|
+
|-----|-----------|-------------|
|
|
126
|
+
| [Qwen Code](https://github.com/QwenLM/qwen-code) | 2,000 requests/day | No |
|
|
127
|
+
| [Rovo Dev CLI](https://www.atlassian.com/rovo) | 5M tokens/day (beta) | No |
|
|
128
|
+
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | 100–250 requests/day | No |
|
|
129
|
+
| [Jules](https://jules.google/) | 15 tasks/day | No |
|
|
130
|
+
| [AWS Kiro](https://kiro.dev/) | 50 credits/month | No |
|
|
131
|
+
| [Trae](https://trae.ai/) | 10 fast + 50 slow requests/month | No |
|
|
132
|
+
| [Codeium](https://codeium.com/) | Unlimited forever, basic models | No |
|
|
133
|
+
| [JetBrains AI Assistant](https://www.jetbrains.com/ai/) | Unlimited completions + local models | No |
|
|
134
|
+
| [Continue.dev](https://www.continue.dev/) | Free VS Code/JetBrains extension, local models via Ollama | No |
|
|
135
|
+
| [Warp](https://warp.dev/) | 150 credits/month (first 2 months), then 75/month | No |
|
|
136
|
+
| [Amazon Q Developer](https://aws.amazon.com/q/developer/) | 50 agentic requests/month | Required |
|
|
137
|
+
| [Windsurf](https://windsurf.com/) | 25 prompt credits/month | Required |
|
|
138
|
+
| [Kilo Code](https://kilocode.ai/) | Up to $25 signup credits (one-time) | Required |
|
|
139
|
+
| [Tabnine](https://www.tabnine.com/) | Basic completions + chat (limited) | Required |
|
|
140
|
+
| [SuperMaven](https://supermaven.com/) | Basic suggestions, 1M token context | Required |
|
|
141
|
+
|
|
142
|
+
### 🔑 API Providers with Permanent Free Tiers
|
|
143
|
+
|
|
144
|
+
| Provider | Free limits | Notable models |
|
|
145
|
+
|----------|-------------|----------------|
|
|
146
|
+
| [OpenRouter](https://openrouter.ai/keys) | 50 req/day, 1K/day with $10 purchase | Qwen3-Coder, Llama 3.3 70B, Gemma 3 |
|
|
147
|
+
| [Google AI Studio](https://aistudio.google.com/apikey) | 5–500 req/day (varies by model) | Gemini 2.5 Flash, Gemma 3 |
|
|
148
|
+
| [NVIDIA NIM](https://build.nvidia.com) | 40 RPM | Llama 3.3 70B, Mistral Large, Qwen3 235B |
|
|
149
|
+
| [Groq](https://console.groq.com/keys) | 1K–14.4K req/day (model-dependent) | Llama 3.3 70B, Llama 4 Scout, Kimi K2 |
|
|
150
|
+
| [Cerebras](https://cloud.cerebras.ai/) | 30 RPM, 1M tokens/day | Qwen3-235B, Llama 3.1 70B, GPT-OSS 120B |
|
|
151
|
+
| [Cohere](https://cohere.com/) | 20 RPM, 1K/month | Command R+, Aya Expanse 32B |
|
|
152
|
+
| [Mistral La Plateforme](https://console.mistral.ai/) | 1 req/s, 1B tokens/month | Mistral Large 3, Small 3.1 |
|
|
153
|
+
| [Cloudflare Workers AI](https://dash.cloudflare.com) | 10K neurons/day | Llama 3.3 70B, QwQ 32B, 47+ models |
|
|
154
|
+
| [GitHub Models](https://github.com/marketplace/models) | Depends on Copilot tier | GPT-4o, DeepSeek-R1, Llama 3.3 |
|
|
155
|
+
| [SiliconFlow](https://cloud.siliconflow.cn/account/ak) | 1K RPM, 50K TPM | Qwen3-8B, DeepSeek-R1, GLM-4.1V |
|
|
156
|
+
| [HuggingFace](https://huggingface.com/settings/tokens) | ~$0.10/month credits | Llama 3.3 70B, Qwen2.5 72B |
|
|
157
|
+
|
|
158
|
+
### 💰 Providers with Trial Credits
|
|
159
|
+
|
|
160
|
+
| Provider | Credits | Duration |
|
|
161
|
+
|----------|---------|----------|
|
|
162
|
+
| [Hyperbolic](https://app.hyperbolic.ai/) | $1 free | Permanent |
|
|
163
|
+
| [Fireworks](https://fireworks.ai/) | $1 | Permanent |
|
|
164
|
+
| [Nebius](https://tokenfactory.nebius.com/) | $1 | Permanent |
|
|
165
|
+
| [SambaNova Cloud](https://cloud.sambanova.ai/) | $5 | 3 months |
|
|
166
|
+
| [AI21](https://studio.ai21.com/) | $10 | 3 months |
|
|
167
|
+
| [Upstage](https://console.upstage.ai/) | $10 | 3 months |
|
|
168
|
+
| [NLP Cloud](https://nlpcloud.com/home) | $15 | Permanent |
|
|
169
|
+
| [Alibaba DashScope](https://bailian.console.alibabacloud.com/) | 1M tokens/model | 90 days |
|
|
170
|
+
| [Scaleway](https://console.scaleway.com/generative-api/models) | 1M tokens | Permanent |
|
|
171
|
+
| [Modal](https://modal.com) | $5/month | Monthly |
|
|
172
|
+
| [Inference.net](https://inference.net) | $1 (+ $25 on survey) | Permanent |
|
|
173
|
+
| [Novita](https://novita.ai/) | $0.5 | 1 year |
|
|
174
|
+
|
|
175
|
+
### 🎓 Free with Education/Developer Programs
|
|
176
|
+
|
|
177
|
+
| Program | What you get |
|
|
178
|
+
|---------|--------------|
|
|
179
|
+
| [GitHub Student Pack](https://education.github.com/pack) | Free Copilot Pro for students (verify with .edu email) |
|
|
180
|
+
| [GitHub Copilot Free](https://code.visualstudio.com/blogs/2024/12/18/free-github-copilot) | 50 chat + 2,000 completions/month in VS Code |
|
|
181
|
+
| [Copilot Pro for teachers/maintainers](https://docs.github.com/en/copilot/how-tos/manage-your-account/get-free-access-to-copilot-pro) | Free Copilot Pro for open source maintainers & educators |
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
105
185
|
### Tier scale
|
|
106
186
|
|
|
107
187
|
| Tier | SWE-bench | Best for |
|
|
@@ -369,6 +449,21 @@ For every model in this tool, **you own the generated output** — code, text, o
|
|
|
369
449
|
|
|
370
450
|
---
|
|
371
451
|
|
|
452
|
+
## 📊 Telemetry
|
|
453
|
+
|
|
454
|
+
`free-coding-models` collects anonymous usage telemetry to help understand how the CLI is used and improve the product. No personal information, API keys, prompts, source code, file paths, or secrets are ever collected.
|
|
455
|
+
|
|
456
|
+
The telemetry payload is limited to anonymous product analytics such as the app version, selected tool mode, operating system, terminal family, and a random anonymous install ID stored locally on your machine. When a model is launched, telemetry can also include the selected tool, provider, model ID, model label, model tier, launch result, and a few product actions such as installing provider catalogs, saving/removing API keys, or toggling shell environment export.
|
|
457
|
+
|
|
458
|
+
Telemetry is enabled by default and can be disabled with any of the following:
|
|
459
|
+
|
|
460
|
+
| Method | How |
|
|
461
|
+
|--------|-----|
|
|
462
|
+
| CLI flag | Run `free-coding-models --no-telemetry` |
|
|
463
|
+
| Environment variable | Set `FREE_CODING_MODELS_TELEMETRY=0` (also supports `false` or `off`) |
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
372
467
|
## 🛡️ Security & Trust
|
|
373
468
|
|
|
374
469
|
### Supply Chain
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.44",
|
|
4
4
|
"description": "Find the fastest coding LLM models in seconds — ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nvidia",
|
package/src/app.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* - `getTelemetryDistinctId`: Generate/reuse a stable anonymous ID for telemetry
|
|
33
33
|
* - `getTelemetryTerminal`: Infer terminal family (Terminal.app, iTerm2, kitty, etc.)
|
|
34
34
|
* - `isTelemetryDebugEnabled` / `telemetryDebug`: Optional runtime telemetry diagnostics via env
|
|
35
|
-
* - `sendUsageTelemetry`: Fire-and-forget anonymous app-start
|
|
35
|
+
* - `sendUsageTelemetry`: Fire-and-forget anonymous app-start, launch, and action events
|
|
36
36
|
* - `ensureFavoritesConfig` / `toggleFavoriteModel`: Persist and toggle pinned favorites
|
|
37
37
|
* - `promptApiKey`: Interactive wizard for first-time multi-provider API key setup
|
|
38
38
|
* - `buildPingRequest` / `ping`: Build provider-specific probe requests and measure latency
|
|
@@ -271,6 +271,8 @@ export async function runApp(cliArgs, config) {
|
|
|
271
271
|
})
|
|
272
272
|
if (requestedMode) mode = requestedMode
|
|
273
273
|
|
|
274
|
+
const sessionId = `session_${randomUUID()}`
|
|
275
|
+
|
|
274
276
|
// 📖 Track app opening early so fast exits are still counted.
|
|
275
277
|
// 📖 Must run before update checks because npm registry lookups can add startup delay.
|
|
276
278
|
void sendUsageTelemetry(config, cliArgs, {
|
|
@@ -278,6 +280,10 @@ export async function runApp(cliArgs, config) {
|
|
|
278
280
|
version: LOCAL_VERSION,
|
|
279
281
|
mode,
|
|
280
282
|
ts: new Date().toISOString(),
|
|
283
|
+
properties: {
|
|
284
|
+
session_id: sessionId,
|
|
285
|
+
event_version: 1,
|
|
286
|
+
},
|
|
281
287
|
})
|
|
282
288
|
|
|
283
289
|
// 📖 Auto-update detection: check npm registry for new versions at startup.
|
|
@@ -419,6 +425,7 @@ export async function runApp(cliArgs, config) {
|
|
|
419
425
|
originFilterMode: 0, // 📖 Index into ORIGIN_CYCLE (0=All, then providers)
|
|
420
426
|
hideUnconfiguredModels: config.settings?.hideUnconfiguredModels === true, // 📖 Hide providers with no configured API key when true.
|
|
421
427
|
favoritesPinnedAndSticky: config.settings?.favoritesPinnedAndSticky === true, // 📖 false by default: favorites follow normal sort/filter rules until Y enables pinned+sticky mode.
|
|
428
|
+
footerHidden: config.settings?.footerHidden === true, // 📖 true = footer is collapsed to a single toggle hint
|
|
422
429
|
scrollOffset: 0, // 📖 First visible model index in viewport
|
|
423
430
|
terminalRows: process.stdout.rows || 24, // 📖 Current terminal height
|
|
424
431
|
terminalCols: process.stdout.columns || 80, // 📖 Current terminal width
|
|
@@ -438,6 +445,7 @@ export async function runApp(cliArgs, config) {
|
|
|
438
445
|
settingsUpdateLatestVersion: null, // 📖 Latest npm version discovered from manual check
|
|
439
446
|
settingsUpdateError: null, // 📖 Last update-check error message for maintenance row
|
|
440
447
|
config, // 📖 Live reference to the config object (updated on save)
|
|
448
|
+
sessionId, // 📖 Per-process analytics link between app_start and later app_use/app_action events.
|
|
441
449
|
visibleSorted: [], // 📖 Cached visible+sorted models — shared between render loop and key handlers
|
|
442
450
|
commandPaletteOpen: false, // 📖 Whether the Ctrl+P command palette overlay is active.
|
|
443
451
|
commandPaletteQuery: '', // 📖 Current command palette search query.
|
|
@@ -863,6 +871,7 @@ export async function runApp(cliArgs, config) {
|
|
|
863
871
|
getToolInstallPlan,
|
|
864
872
|
isToolInstalled,
|
|
865
873
|
installToolWithPlan,
|
|
874
|
+
sendUsageTelemetry,
|
|
866
875
|
startRecommendAnalysis: overlays.startRecommendAnalysis,
|
|
867
876
|
stopRecommendAnalysis: overlays.stopRecommendAnalysis,
|
|
868
877
|
sendBugReport,
|
|
@@ -1112,7 +1121,7 @@ export async function runApp(cliArgs, config) {
|
|
|
1112
1121
|
pinFavorites: state.favoritesPinnedAndSticky,
|
|
1113
1122
|
})
|
|
1114
1123
|
|
|
1115
|
-
process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate))
|
|
1124
|
+
process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate, state.footerHidden))
|
|
1116
1125
|
if (process.stdout.isTTY) {
|
|
1117
1126
|
process.stdout.flush && process.stdout.flush()
|
|
1118
1127
|
}
|
package/src/command-palette.js
CHANGED
|
@@ -33,6 +33,7 @@ const TOOL_MODE_DESCRIPTIONS = {
|
|
|
33
33
|
cline: 'Launch Cline CLI with the selected model.',
|
|
34
34
|
rovo: 'Rovo Dev CLI model (launch with Rovo tool only).',
|
|
35
35
|
gemini: 'Gemini CLI model (launch with Gemini tool only).',
|
|
36
|
+
jcode: 'Launch jcode coding agent with the selected model.',
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const TOOL_MODE_COMMANDS = TOOL_MODE_ORDER.map((toolMode) => {
|
package/src/config.js
CHANGED
|
@@ -212,6 +212,7 @@ function normalizeSettingsSection(settings) {
|
|
|
212
212
|
hideUnconfiguredModels: typeof safeSettings.hideUnconfiguredModels === 'boolean' ? safeSettings.hideUnconfiguredModels : true,
|
|
213
213
|
favoritesPinnedAndSticky: typeof safeSettings.favoritesPinnedAndSticky === 'boolean' ? safeSettings.favoritesPinnedAndSticky : false,
|
|
214
214
|
theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : 'auto',
|
|
215
|
+
footerHidden: typeof safeSettings.footerHidden === 'boolean' ? safeSettings.footerHidden : false,
|
|
215
216
|
}
|
|
216
217
|
}
|
|
217
218
|
|
|
@@ -839,7 +840,7 @@ export function isProviderEnabled(config, providerKey) {
|
|
|
839
840
|
/**
|
|
840
841
|
* 📖 _emptyProfileSettings: Default TUI settings.
|
|
841
842
|
*
|
|
842
|
-
* @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number, hideUnconfiguredModels: boolean, favoritesPinnedAndSticky: boolean, preferredToolMode: string }}
|
|
843
|
+
* @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number, hideUnconfiguredModels: boolean, favoritesPinnedAndSticky: boolean, preferredToolMode: string, theme: string, footerHidden: boolean }}
|
|
843
844
|
*/
|
|
844
845
|
export function _emptyProfileSettings() {
|
|
845
846
|
return {
|
|
@@ -851,6 +852,7 @@ export function _emptyProfileSettings() {
|
|
|
851
852
|
favoritesPinnedAndSticky: false, // 📖 default mode keeps favorites as normal starred rows; press Y to pin+stick them.
|
|
852
853
|
preferredToolMode: 'opencode', // 📖 remember the last Z-selected launcher across app restarts
|
|
853
854
|
theme: 'auto', // 📖 'auto' follows the terminal/OS theme, override with 'dark' or 'light' if needed
|
|
855
|
+
footerHidden: false, // 📖 false = full footer shown; true = collapsed to a single "(W) Toggle Footer" hint
|
|
854
856
|
}
|
|
855
857
|
}
|
|
856
858
|
|
package/src/key-handler.js
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* 📖 Key I opens the unified "Feedback, bugs & requests" overlay.
|
|
12
12
|
*
|
|
13
13
|
* It also owns the "test key" model selection used by the Settings overlay.
|
|
14
|
+
* Anonymous telemetry hooks for model launches and a few high-signal settings
|
|
15
|
+
* actions live here too, because this module already sees the final effective
|
|
16
|
+
* tool mode, provider, and selected model right before the app hands control
|
|
17
|
+
* to an external CLI.
|
|
14
18
|
* Some providers expose models in `/v1/models` that are not actually callable
|
|
15
19
|
* on the chat-completions endpoint. To avoid false negatives when a user
|
|
16
20
|
* presses `T` in Settings, the helpers below discover candidate model IDs,
|
|
@@ -195,6 +199,7 @@ export function createKeyHandler(ctx) {
|
|
|
195
199
|
getToolInstallPlan,
|
|
196
200
|
isToolInstalled,
|
|
197
201
|
installToolWithPlan,
|
|
202
|
+
sendUsageTelemetry,
|
|
198
203
|
startRecommendAnalysis,
|
|
199
204
|
stopRecommendAnalysis,
|
|
200
205
|
sendBugReport,
|
|
@@ -232,6 +237,56 @@ export function createKeyHandler(ctx) {
|
|
|
232
237
|
return mode !== 'opencode-desktop'
|
|
233
238
|
}
|
|
234
239
|
|
|
240
|
+
function getModelTelemetryFamily(providerKey) {
|
|
241
|
+
if (providerKey === 'rovo' || providerKey === 'gemini' || providerKey === 'opencode-zen') return providerKey
|
|
242
|
+
return 'standard'
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function trackTelemetryEvent(event, properties = {}) {
|
|
246
|
+
if (typeof sendUsageTelemetry !== 'function') return
|
|
247
|
+
void sendUsageTelemetry(state.config, cliArgs, {
|
|
248
|
+
event,
|
|
249
|
+
mode: state.mode,
|
|
250
|
+
ts: new Date().toISOString(),
|
|
251
|
+
properties: {
|
|
252
|
+
session_id: state.sessionId,
|
|
253
|
+
event_version: 1,
|
|
254
|
+
...properties,
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function buildModelLaunchTelemetry(selected, extra = {}) {
|
|
260
|
+
return {
|
|
261
|
+
action_type: 'launch_model',
|
|
262
|
+
tool_mode: state.mode,
|
|
263
|
+
provider_key: selected.providerKey,
|
|
264
|
+
model_id: selected.modelId,
|
|
265
|
+
model_label: selected.label,
|
|
266
|
+
model_tier: selected.tier,
|
|
267
|
+
model_family: getModelTelemetryFamily(selected.providerKey),
|
|
268
|
+
...extra,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function trackAppUse(selected, extra = {}) {
|
|
273
|
+
trackTelemetryEvent('app_use', buildModelLaunchTelemetry(selected, extra))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function trackAppUseResult(selected, launchResult, extra = {}) {
|
|
277
|
+
trackTelemetryEvent('app_use_result', buildModelLaunchTelemetry(selected, {
|
|
278
|
+
launch_result: launchResult,
|
|
279
|
+
...extra,
|
|
280
|
+
}))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function trackAppAction(actionType, properties = {}) {
|
|
284
|
+
trackTelemetryEvent('app_action', {
|
|
285
|
+
action_type: actionType,
|
|
286
|
+
...properties,
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
235
290
|
async function launchSelectedModel(selected, options = {}) {
|
|
236
291
|
const { uiAlreadyStopped = false } = options
|
|
237
292
|
userSelected = { modelId: selected.modelId, label: selected.label, tier: selected.tier, providerKey: selected.providerKey }
|
|
@@ -264,6 +319,9 @@ export function createKeyHandler(ctx) {
|
|
|
264
319
|
|
|
265
320
|
// 📖 Case A: User is in Rovo/Gemini mode but selected a model from a different provider
|
|
266
321
|
if (isActiveModeCliOnly && !modelBelongsToActiveMode) {
|
|
322
|
+
trackAppUseResult(selected, 'blocked_incompatible_model', {
|
|
323
|
+
blocked_by_tool_mode: state.mode,
|
|
324
|
+
})
|
|
267
325
|
const availableModels = MODELS.filter(m => m[5] === state.mode)
|
|
268
326
|
console.log(chalk.yellow(` ⚠ ${activeMeta.label} can only launch its own models.`))
|
|
269
327
|
console.log(chalk.yellow(` "${selected.label}" is not a ${activeMeta.label} model.`))
|
|
@@ -331,6 +389,9 @@ export function createKeyHandler(ctx) {
|
|
|
331
389
|
|
|
332
390
|
const installResult = await installToolWithPlan(installPlan)
|
|
333
391
|
if (!installResult.ok) {
|
|
392
|
+
trackAppUseResult(selected, 'blocked_missing_tool', {
|
|
393
|
+
required_tool_mode: toolModeForProvider,
|
|
394
|
+
})
|
|
334
395
|
console.log(chalk.red(` X Tool installation failed with exit code ${installResult.exitCode}.`))
|
|
335
396
|
if (installPlan.docsUrl) console.log(chalk.dim(` Docs: ${installPlan.docsUrl}`))
|
|
336
397
|
console.log()
|
|
@@ -339,6 +400,9 @@ export function createKeyHandler(ctx) {
|
|
|
339
400
|
|
|
340
401
|
// 📖 Verify tool is now installed
|
|
341
402
|
if (!isToolInstalled(toolModeForProvider)) {
|
|
403
|
+
trackAppUseResult(selected, 'blocked_missing_tool', {
|
|
404
|
+
required_tool_mode: toolModeForProvider,
|
|
405
|
+
})
|
|
342
406
|
console.log(chalk.yellow(' ⚠ The installer finished, but the tool is still not reachable from this terminal session.'))
|
|
343
407
|
console.log(chalk.dim(' Restart your shell or add the tool bin directory to PATH, then retry the launch.'))
|
|
344
408
|
if (installPlan.docsUrl) console.log(chalk.dim(` Docs: ${installPlan.docsUrl}`))
|
|
@@ -351,6 +415,10 @@ export function createKeyHandler(ctx) {
|
|
|
351
415
|
}
|
|
352
416
|
}
|
|
353
417
|
|
|
418
|
+
const launchSource = uiAlreadyStopped ? 'tool_install_retry' : 'tui_enter'
|
|
419
|
+
trackAppUse(selected, { launch_source: launchSource })
|
|
420
|
+
trackAppUseResult(selected, 'started', { launch_source: launchSource })
|
|
421
|
+
|
|
354
422
|
let exitCode = 0
|
|
355
423
|
if (state.mode === 'openclaw') {
|
|
356
424
|
exitCode = await startOpenClaw(userSelected, state.config, { launchCli: true })
|
|
@@ -377,6 +445,9 @@ export function createKeyHandler(ctx) {
|
|
|
377
445
|
|
|
378
446
|
const installResult = await installToolWithPlan(currentPlan)
|
|
379
447
|
if (!installResult.ok) {
|
|
448
|
+
trackAppUseResult(selected, 'blocked_missing_tool', {
|
|
449
|
+
required_tool_mode: state.mode,
|
|
450
|
+
})
|
|
380
451
|
console.log(chalk.red(` X Tool installation failed with exit code ${installResult.exitCode}.`))
|
|
381
452
|
if (currentPlan?.docsUrl) console.log(chalk.dim(` Docs: ${currentPlan.docsUrl}`))
|
|
382
453
|
console.log()
|
|
@@ -384,6 +455,9 @@ export function createKeyHandler(ctx) {
|
|
|
384
455
|
}
|
|
385
456
|
|
|
386
457
|
if (shouldCheckMissingTool(state.mode) && !isToolInstalled(state.mode)) {
|
|
458
|
+
trackAppUseResult(selected, 'blocked_missing_tool', {
|
|
459
|
+
required_tool_mode: state.mode,
|
|
460
|
+
})
|
|
387
461
|
console.log(chalk.yellow(' ⚠ The installer finished, but the tool is still not reachable from this terminal session.'))
|
|
388
462
|
console.log(chalk.dim(' Restart your shell or add the tool bin directory to PATH, then retry the launch.'))
|
|
389
463
|
if (currentPlan?.docsUrl) console.log(chalk.dim(` Docs: ${currentPlan.docsUrl}`))
|
|
@@ -565,6 +639,9 @@ export function createKeyHandler(ctx) {
|
|
|
565
639
|
} else {
|
|
566
640
|
removeShellEnv()
|
|
567
641
|
}
|
|
642
|
+
trackAppAction('shell_env_export_toggled', {
|
|
643
|
+
enabled: state.config.settings.shellEnvEnabled === true,
|
|
644
|
+
})
|
|
568
645
|
}
|
|
569
646
|
|
|
570
647
|
function resetInstallEndpointsOverlay() {
|
|
@@ -605,6 +682,14 @@ export function createKeyHandler(ctx) {
|
|
|
605
682
|
...(result.extraPath ? [chalk.bold(`Secrets:`) + ` ${result.extraPath}`] : []),
|
|
606
683
|
],
|
|
607
684
|
}
|
|
685
|
+
trackAppAction('install_provider_endpoints', {
|
|
686
|
+
provider_key: result.providerKey,
|
|
687
|
+
tool_mode: result.toolMode,
|
|
688
|
+
install_scope: result.scope,
|
|
689
|
+
connection_mode: state.installEndpointsConnectionMode || 'direct',
|
|
690
|
+
model_count: result.modelCount,
|
|
691
|
+
selected_model_count: selectedModelIds.length,
|
|
692
|
+
})
|
|
608
693
|
state.installEndpointsPhase = 'result'
|
|
609
694
|
state.installEndpointsCursor = 0
|
|
610
695
|
state.installEndpointsScrollOffset = 0
|
|
@@ -1880,6 +1965,11 @@ export function createKeyHandler(ctx) {
|
|
|
1880
1965
|
if (!saveResult.success) {
|
|
1881
1966
|
state.settingsErrorMsg = `⚠️ Failed to persist ${pk} API key: ${saveResult.error || 'Unknown error'}`
|
|
1882
1967
|
setTimeout(() => { state.settingsErrorMsg = null }, 4000)
|
|
1968
|
+
} else {
|
|
1969
|
+
trackAppAction('api_key_saved', {
|
|
1970
|
+
provider_key: pk,
|
|
1971
|
+
key_action: state.settingsAddKeyMode ? 'add' : 'replace',
|
|
1972
|
+
})
|
|
1883
1973
|
}
|
|
1884
1974
|
}
|
|
1885
1975
|
state.settingsEditMode = false
|
|
@@ -2111,6 +2201,10 @@ export function createKeyHandler(ctx) {
|
|
|
2111
2201
|
? `✅ Removed one key for ${pk} (${remaining} remaining)`
|
|
2112
2202
|
: `✅ Removed last API key for ${pk}`
|
|
2113
2203
|
state.settingsSyncStatus = { type: 'success', msg }
|
|
2204
|
+
trackAppAction('api_key_removed', {
|
|
2205
|
+
provider_key: pk,
|
|
2206
|
+
remaining_key_count: remaining,
|
|
2207
|
+
})
|
|
2114
2208
|
}
|
|
2115
2209
|
return
|
|
2116
2210
|
}
|
|
@@ -2191,10 +2285,20 @@ export function createKeyHandler(ctx) {
|
|
|
2191
2285
|
// 📖 W cycles the supported ping modes:
|
|
2192
2286
|
// 📖 speed (2s) → normal (10s) → slow (30s) → forced (4s) → speed.
|
|
2193
2287
|
// 📖 forced ignores auto speed/slow transitions until the user leaves it manually.
|
|
2194
|
-
if (key.name === 'w') {
|
|
2288
|
+
if (key.name === 'w' && !key.alt && !key.ctrl && !key.meta) {
|
|
2195
2289
|
const currentIdx = PING_MODE_CYCLE.indexOf(state.pingMode)
|
|
2196
2290
|
const nextIdx = currentIdx >= 0 ? (currentIdx + 1) % PING_MODE_CYCLE.length : 0
|
|
2197
2291
|
setPingMode(PING_MODE_CYCLE[nextIdx], 'manual')
|
|
2292
|
+
return
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// 📖 Alt+W: toggle footer visibility (collapse to single hint when hidden)
|
|
2296
|
+
if (key.name === 'w' && key.alt && !key.ctrl && !key.meta) {
|
|
2297
|
+
state.footerHidden = !state.footerHidden
|
|
2298
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
2299
|
+
state.config.settings.footerHidden = state.footerHidden
|
|
2300
|
+
saveConfig(state.config)
|
|
2301
|
+
return
|
|
2198
2302
|
}
|
|
2199
2303
|
|
|
2200
2304
|
// 📖 E toggles hiding models whose provider has no configured API key.
|
package/src/openclaw.js
CHANGED
|
@@ -33,6 +33,8 @@ import { installProviderEndpoints } from './endpoint-installer.js'
|
|
|
33
33
|
import { ENV_VAR_NAMES } from './provider-metadata.js'
|
|
34
34
|
import { PROVIDER_COLOR } from './render-table.js'
|
|
35
35
|
import { resolveToolBinaryPath } from './tool-bootstrap.js'
|
|
36
|
+
import { getApiKey } from './config.js'
|
|
37
|
+
import { syncShellEnv } from './shell-env.js'
|
|
36
38
|
|
|
37
39
|
const OPENCLAW_CONFIG = join(homedir(), '.openclaw', 'openclaw.json')
|
|
38
40
|
|
|
@@ -56,7 +58,14 @@ export function saveOpenClawConfig(config, options = {}) {
|
|
|
56
58
|
writeFileSync(filePath, JSON.stringify(config, null, 2))
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
/**
|
|
62
|
+
* 📖 Spawn OpenClaw CLI with proper environment including API key.
|
|
63
|
+
* OpenClaw reads API keys from environment variables at runtime, not just config file.
|
|
64
|
+
*
|
|
65
|
+
* @param {NodeJS.ProcessEnv} env - Environment variables including API key
|
|
66
|
+
* @returns {Promise<number>} Exit code
|
|
67
|
+
*/
|
|
68
|
+
function spawnOpenClawCli(env = process.env) {
|
|
60
69
|
return new Promise(async (resolve, reject) => {
|
|
61
70
|
const { spawn } = await import('child_process')
|
|
62
71
|
const command = resolveToolBinaryPath('openclaw') || 'openclaw'
|
|
@@ -64,7 +73,7 @@ function spawnOpenClawCli() {
|
|
|
64
73
|
stdio: 'inherit',
|
|
65
74
|
shell: false,
|
|
66
75
|
detached: false,
|
|
67
|
-
env
|
|
76
|
+
env,
|
|
68
77
|
})
|
|
69
78
|
|
|
70
79
|
child.on('exit', (code) => resolve(typeof code === 'number' ? code : 0))
|
|
@@ -107,16 +116,29 @@ export async function startOpenClaw(model, config, options = {}) {
|
|
|
107
116
|
})
|
|
108
117
|
|
|
109
118
|
const providerEnvName = ENV_VAR_NAMES[model.providerKey]
|
|
119
|
+
const apiKey = getApiKey(config, model.providerKey)
|
|
110
120
|
console.log(chalk.rgb(255, 140, 0)(` ✓ Default model set to: ${result.primaryModelRef || `${result.providerId}/${model.modelId}`}`))
|
|
111
121
|
console.log()
|
|
112
122
|
console.log(chalk.dim(` 📄 Config updated: ${result.path}`))
|
|
113
123
|
if (result.backupPath) console.log(chalk.dim(` 💾 Backup: ${result.backupPath}`))
|
|
114
124
|
if (providerEnvName) console.log(chalk.dim(` 🔑 API key synced under config env.${providerEnvName}`))
|
|
115
125
|
console.log()
|
|
126
|
+
|
|
116
127
|
if (options.launchCli) {
|
|
128
|
+
// 📖 Sync shell env so API key is available as environment variable
|
|
129
|
+
if (config.settings?.shellEnvEnabled !== false) {
|
|
130
|
+
syncShellEnv(config)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 📖 Build env with API key so OpenClaw can authenticate
|
|
134
|
+
const launchEnv = { ...process.env }
|
|
135
|
+
if (apiKey && providerEnvName) {
|
|
136
|
+
launchEnv[providerEnvName] = apiKey
|
|
137
|
+
}
|
|
138
|
+
|
|
117
139
|
console.log(chalk.dim(' Starting OpenClaw...'))
|
|
118
140
|
console.log()
|
|
119
|
-
await spawnOpenClawCli()
|
|
141
|
+
await spawnOpenClawCli(launchEnv)
|
|
120
142
|
} else {
|
|
121
143
|
console.log(chalk.dim(' 💡 OpenClaw will reload config automatically when it notices the file change.'))
|
|
122
144
|
console.log(chalk.dim(` To apply manually: openclaw models set ${result.primaryModelRef || `${result.providerId}/${model.modelId}`}`))
|
package/src/opencode.js
CHANGED
|
@@ -465,6 +465,20 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
465
465
|
options: { baseURL: 'https://apis.iflow.cn/v1', apiKey: '{env:IFLOW_API_KEY}' },
|
|
466
466
|
models: {}
|
|
467
467
|
}
|
|
468
|
+
} else if (providerKey === 'chutes') {
|
|
469
|
+
config.provider.chutes = {
|
|
470
|
+
npm: '@ai-sdk/openai-compatible',
|
|
471
|
+
name: 'Chutes AI',
|
|
472
|
+
options: { baseURL: 'https://chutes.ai/v1', apiKey: '{env:CHUTES_API_KEY}' },
|
|
473
|
+
models: {}
|
|
474
|
+
}
|
|
475
|
+
} else if (providerKey === 'ovhcloud') {
|
|
476
|
+
config.provider.ovhcloud = {
|
|
477
|
+
npm: '@ai-sdk/openai-compatible',
|
|
478
|
+
name: 'OVHcloud AI',
|
|
479
|
+
options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
|
|
480
|
+
models: {}
|
|
481
|
+
}
|
|
468
482
|
}
|
|
469
483
|
}
|
|
470
484
|
|
|
@@ -724,6 +738,20 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
|
|
|
724
738
|
options: { baseURL: 'https://apis.iflow.cn/v1', apiKey: '{env:IFLOW_API_KEY}' },
|
|
725
739
|
models: {}
|
|
726
740
|
}
|
|
741
|
+
} else if (providerKey === 'chutes') {
|
|
742
|
+
config.provider.chutes = {
|
|
743
|
+
npm: '@ai-sdk/openai-compatible',
|
|
744
|
+
name: 'Chutes AI',
|
|
745
|
+
options: { baseURL: 'https://chutes.ai/v1', apiKey: '{env:CHUTES_API_KEY}' },
|
|
746
|
+
models: {}
|
|
747
|
+
}
|
|
748
|
+
} else if (providerKey === 'ovhcloud') {
|
|
749
|
+
config.provider.ovhcloud = {
|
|
750
|
+
npm: '@ai-sdk/openai-compatible',
|
|
751
|
+
name: 'OVHcloud AI',
|
|
752
|
+
options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
|
|
753
|
+
models: {}
|
|
754
|
+
}
|
|
727
755
|
}
|
|
728
756
|
}
|
|
729
757
|
|
package/src/render-table.js
CHANGED
|
@@ -104,7 +104,7 @@ export const PROVIDER_COLOR = new Proxy({}, {
|
|
|
104
104
|
})
|
|
105
105
|
|
|
106
106
|
// ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
|
|
107
|
-
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null) {
|
|
107
|
+
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null, footerHidden = false) {
|
|
108
108
|
// 📖 Filter out hidden models for display
|
|
109
109
|
const visibleResults = results.filter(r => !r.hidden)
|
|
110
110
|
|
|
@@ -780,7 +780,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
780
780
|
|
|
781
781
|
// 📖 Line 2: command palette, recommend, feedback, theme
|
|
782
782
|
{
|
|
783
|
-
const cpText = '
|
|
783
|
+
const cpText = ' CTRL+P ⚡️ Command Palette '
|
|
784
784
|
const parts = [
|
|
785
785
|
{ text: ' ', key: null },
|
|
786
786
|
{ text: cpText, key: 'ctrl+p' },
|
|
@@ -802,7 +802,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
802
802
|
|
|
803
803
|
// 📖 Line 2: command palette (highlighted as new), recommend, feedback, and extended hints.
|
|
804
804
|
// 📖 CTRL+P ⚡️ Command Palette uses neon-green-on-dark-green background to highlight the feature.
|
|
805
|
-
const paletteLabel = chalk.bgRgb(0, 60, 0).rgb(57, 255, 20).bold('
|
|
805
|
+
const paletteLabel = chalk.bgRgb(0, 60, 0).rgb(57, 255, 20).bold(' CTRL+P ⚡️ Command Palette ')
|
|
806
806
|
lines.push(
|
|
807
807
|
' ' + paletteLabel + themeColors.dim(` • `) +
|
|
808
808
|
hotkey('Q', ' Smart Recommend') + themeColors.dim(` • `) +
|
|
@@ -878,27 +878,35 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
878
878
|
|
|
879
879
|
_lastLayout.footerHotkeys = footerHotkeys
|
|
880
880
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
:
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
)
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
881
|
+
if (footerHidden) {
|
|
882
|
+
// 📖 Collapsed footer: single line with toggle hint
|
|
883
|
+
lines.push(
|
|
884
|
+
' ' + themeColors.hotkey('Alt+W') + themeColors.dim(' Toggle Footer') +
|
|
885
|
+
themeColors.dim(' • Ctrl+C Exit')
|
|
886
|
+
)
|
|
887
|
+
} else {
|
|
888
|
+
const releaseLabel = lastReleaseDate
|
|
889
|
+
? chalk.rgb(255, 182, 193)(`Last release: ${lastReleaseDate}`)
|
|
890
|
+
: ''
|
|
891
|
+
|
|
892
|
+
lines.push(
|
|
893
|
+
' ' + themeColors.hotkey('N') + themeColors.dim(' Changelog') +
|
|
894
|
+
(filterBadge
|
|
895
|
+
? themeColors.dim(' • ') + filterBadge
|
|
896
|
+
: '') +
|
|
897
|
+
themeColors.dim(' • ') +
|
|
898
|
+
themeColors.dim('Ctrl+C Exit') +
|
|
899
|
+
(releaseLabel ? themeColors.dim(' • ') + releaseLabel : '')
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
// 📖 Discord link at the very bottom of the TUI
|
|
903
|
+
lines.push(
|
|
904
|
+
' 💬 ' +
|
|
905
|
+
themeColors.footerDiscord('\x1b]8;;https://discord.gg/ZTNFHvvCkU\x1b\\Join the Discord\x1b]8;;\x1b\\') +
|
|
906
|
+
themeColors.dim(' → ') +
|
|
907
|
+
themeColors.footerDiscord('https://discord.gg/ZTNFHvvCkU')
|
|
908
|
+
)
|
|
909
|
+
}
|
|
902
910
|
|
|
903
911
|
// 📖 Append \x1b[K (erase to EOL) to each line so leftover chars from previous
|
|
904
912
|
// 📖 frames are cleared. Then pad with blank cleared lines to fill the terminal,
|