free-coding-models 0.5.80 โ 0.5.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -34
- package/bin/free-coding-models.js +20 -0
- package/changelog/v0.5.81.md +25 -0
- package/changelog/v0.5.82.md +23 -0
- package/changelog/v0.5.83.md +22 -0
- package/package.json +2 -2
- package/sources.js +163 -64
- package/src/core/benchmark.js +1 -1
- package/src/core/config.js +4 -0
- package/src/core/endpoint-installer.js +7 -2
- package/src/core/kilo.js +8 -1
- package/src/core/models-dev-index.js +1 -0
- package/src/core/opencode.js +16 -2
- package/src/core/ping.js +14 -4
- package/src/core/provider-key-tester.js +5 -2
- package/src/core/provider-metadata.js +36 -9
- package/src/core/quota-capabilities.js +4 -0
- package/src/core/router-daemon.js +2 -2
- package/src/core/security.js +132 -65
- package/src/core/sync-set.js +1 -1
- package/src/core/tool-launchers.js +15 -5
- package/src/core/utils.js +88 -1
- package/src/tui/app.js +7 -5
- package/src/tui/cli-help.js +1 -0
- package/src/tui/command-palette.js +3 -2
- package/src/tui/key-handler.js +172 -61
- package/src/tui/overlays.js +135 -94
- package/src/tui/render-helpers.js +42 -3
- package/src/tui/render-table.js +74 -4
- package/src/tui/theme.js +6 -0
- package/src/tui/tui-state.js +15 -1
- package/web/dist/assets/{index-B5mIBUi5.js โ index-CSoKc1qF.js} +4 -4
- package/web/dist/index.html +1 -1
|
@@ -62,6 +62,10 @@ export const ENV_VAR_NAMES = {
|
|
|
62
62
|
routeway: 'ROUTEWAY_API_KEY',
|
|
63
63
|
novita: 'NOVITA_API_KEY',
|
|
64
64
|
'ollama-cloud': 'OLLAMA_API_KEY',
|
|
65
|
+
pollinations: 'POLLINATIONS_API_KEY',
|
|
66
|
+
siliconflow: 'SILICONFLOW_API_KEY',
|
|
67
|
+
requesty: 'REQUESTY_API_KEY',
|
|
68
|
+
orcarouter: 'ORCAROUTER_API_KEY',
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
// ๐ OPENCODE_MODEL_MAP: sparse table of model IDs that differ between sources.js and OpenCode's
|
|
@@ -201,13 +205,6 @@ export const PROVIDER_METADATA = {
|
|
|
201
205
|
signupHint: 'Get API key',
|
|
202
206
|
rateLimits: 'Gemini free quotas vary by model and region',
|
|
203
207
|
},
|
|
204
|
-
siliconflow: {
|
|
205
|
-
label: 'SiliconFlow',
|
|
206
|
-
color: chalk.rgb(178, 235, 242),
|
|
207
|
-
signupUrl: 'https://cloud.siliconflow.cn/account/ak',
|
|
208
|
-
signupHint: 'API Keys โ Create',
|
|
209
|
-
rateLimits: 'Free models: usually 100 RPM, varies by model',
|
|
210
|
-
},
|
|
211
208
|
together: {
|
|
212
209
|
label: 'Together AI',
|
|
213
210
|
color: chalk.rgb(255, 241, 118),
|
|
@@ -259,11 +256,11 @@ export const PROVIDER_METADATA = {
|
|
|
259
256
|
rateLimits: 'Depends on provider subscription (e.g., Anthropic, OpenAI)',
|
|
260
257
|
},
|
|
261
258
|
'opencode-zen': {
|
|
262
|
-
label: '
|
|
259
|
+
label: 'OpencodeZen',
|
|
263
260
|
color: chalk.rgb(139, 92, 246), // violet โ distinctive from other providers
|
|
264
261
|
signupUrl: 'https://opencode.ai/auth',
|
|
265
262
|
signupHint: 'Login at opencode.ai/auth to get your Zen API key',
|
|
266
|
-
rateLimits: 'Free tier models โ requires
|
|
263
|
+
rateLimits: 'Free tier models โ requires Zen API key',
|
|
267
264
|
zenOnly: true,
|
|
268
265
|
},
|
|
269
266
|
chutes: {
|
|
@@ -312,6 +309,36 @@ export const PROVIDER_METADATA = {
|
|
|
312
309
|
rateLimits: 'Only zero-price live chat models are listed; other Novita models are paid/trial-credit',
|
|
313
310
|
paidProviderNote: 'paid โ 3 free models',
|
|
314
311
|
},
|
|
312
|
+
pollinations: {
|
|
313
|
+
label: 'Pollinations AI โ ๏ธ experimental',
|
|
314
|
+
color: chalk.rgb(255, 105, 180),
|
|
315
|
+
signupUrl: 'https://enter.pollinations.ai',
|
|
316
|
+
signupHint: 'enter.pollinations.ai โ API Keys (free Pollen daily; star github.com/pollinations/pollinations to earn Pollen; 0 Pollen = 402; no purchase needed)',
|
|
317
|
+
rateLimits: 'Free via Pollen (daily grants; ~0.0001-0.003 Pollen/req; 0 Pollen = 402; experimental, system may change)',
|
|
318
|
+
detailedLimits: 'Pollen โ $1. Daily free Pollen via tier (seed/flower/nectar) + quests (star repo, help issue). No purchase required. Cost per request 0.0001-0.003 Pollen. If balance is 0.0000, every MISS returns 402 PAYMENT_REQUIRED and the TUI shows 402; HIT (Cloudflare cache) may still return 200 but is not reliable. Check balance at enter.pollinations.ai โ API Keys. Grants reset daily at midnight UTC. Integration is experimental and not yet confirmed as stable.',
|
|
319
|
+
},
|
|
320
|
+
siliconflow: {
|
|
321
|
+
label: 'SiliconFlow',
|
|
322
|
+
color: chalk.rgb(178, 235, 242),
|
|
323
|
+
signupUrl: 'https://cloud.siliconflow.cn/account/ak',
|
|
324
|
+
signupHint: 'API Keys โ Create (phone SMS verification required)',
|
|
325
|
+
rateLimits: 'Free ยท 3 models @ $0 ยท 1000 RPM ยท 1M tokens/day per model',
|
|
326
|
+
},
|
|
327
|
+
requesty: {
|
|
328
|
+
label: 'Requesty',
|
|
329
|
+
color: chalk.rgb(100, 149, 255),
|
|
330
|
+
signupUrl: 'https://app.requesty.ai/api-keys',
|
|
331
|
+
signupHint: 'API Keys โ Create (200 req/day free, no card)',
|
|
332
|
+
rateLimits: 'Free ยท 200 req/day on free models (20 req/min) ยท no card',
|
|
333
|
+
},
|
|
334
|
+
orcarouter: {
|
|
335
|
+
label: 'OrcaRouter',
|
|
336
|
+
color: chalk.rgb(255, 138, 64),
|
|
337
|
+
signupUrl: 'https://www.orcarouter.ai',
|
|
338
|
+
signupHint: 'Register (GitHub OAuth, no credit card) โ API keys',
|
|
339
|
+
rateLimits: 'Free Hacker tier ยท zero token markup ยท 3 API keys',
|
|
340
|
+
detailedLimits: 'Zero-markup AI gateway: token prices are passed through at provider rates.\nFree Hacker plan: 3 API keys, adaptive routing + automatic failover + guardrails included.\nOnly the explicit $-0 models are listed in this catalog; the orcarouter/fusion family is pay-as-you-go.',
|
|
341
|
+
},
|
|
315
342
|
'ollama-cloud': {
|
|
316
343
|
label: 'Ollama Cloud',
|
|
317
344
|
color: chalk.rgb(230, 230, 230),
|
|
@@ -61,6 +61,10 @@ export const PROVIDER_CAPABILITIES = {
|
|
|
61
61
|
routeway: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
|
|
62
62
|
novita: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
|
|
63
63
|
'ollama-cloud': { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'rolling' },
|
|
64
|
+
pollinations: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'daily' },
|
|
65
|
+
siliconflow: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'daily' },
|
|
66
|
+
requesty: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'daily' },
|
|
67
|
+
orcarouter: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
/** Fallback for unrecognized providers */
|
|
@@ -578,7 +578,7 @@ export function cloneHeadersForUpstream(reqHeaders, apiKey, providerKey) {
|
|
|
578
578
|
}
|
|
579
579
|
headers['Content-Type'] = headers['Content-Type'] || 'application/json'
|
|
580
580
|
headers.Authorization = `Bearer ${apiKey}`
|
|
581
|
-
if (providerKey === 'openrouter') {
|
|
581
|
+
if (providerKey === 'openrouter' || providerKey === 'orcarouter') {
|
|
582
582
|
headers['HTTP-Referer'] = 'https://github.com/vava-nessa/free-coding-models'
|
|
583
583
|
headers['X-Title'] = 'free-coding-models'
|
|
584
584
|
}
|
|
@@ -3619,7 +3619,7 @@ function createDefaultProbeFn(apiKeys) {
|
|
|
3619
3619
|
headers.Prefer = 'wait=4'
|
|
3620
3620
|
} else {
|
|
3621
3621
|
headers.Authorization = `Bearer ${apiKey}`
|
|
3622
|
-
if (provider === 'openrouter') {
|
|
3622
|
+
if (provider === 'openrouter' || provider === 'orcarouter') {
|
|
3623
3623
|
headers['HTTP-Referer'] = 'https://github.com/vava-nessa/free-coding-models'
|
|
3624
3624
|
headers['X-Title'] = 'free-coding-models'
|
|
3625
3625
|
}
|
package/src/core/security.js
CHANGED
|
@@ -8,27 +8,33 @@
|
|
|
8
8
|
* ๐ This module:
|
|
9
9
|
* - Checks config file permissions on startup
|
|
10
10
|
* - Warns user if permissions are too open
|
|
11
|
-
* - Offers auto-fix option with user confirmation
|
|
11
|
+
* - Offers auto-fix option with user confirmation (interactive TTY only)
|
|
12
12
|
* - Fixes permissions securely (chmod 600 = user read/write only)
|
|
13
13
|
*
|
|
14
|
+
* ๐ Issue #173: this check used to run un-awaited inside runApp, so the TUI
|
|
15
|
+
* entered raw mode / the alternate screen while the prompt was still pending.
|
|
16
|
+
* On Windows the warning + prompt were invisible and the app looked frozen.
|
|
17
|
+
* Now checkConfigSecurity() is async, awaited by the bin entry BEFORE the TUI
|
|
18
|
+
* starts, and it never prompts on non-TTY stdin or daemon/web/JSON surfaces.
|
|
19
|
+
*
|
|
14
20
|
* ๐ Secure permissions:
|
|
15
21
|
* - 0o600 (octal 600) = user:rw, group:---, world:---
|
|
16
22
|
* - Only the file owner can read or write
|
|
17
23
|
* - This is the standard for files containing secrets (SSH keys, API keys, etc.)
|
|
18
24
|
*
|
|
19
|
-
* ๐
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
22
|
-
* - Backup tools: Might copy files with permissions intact
|
|
25
|
+
* ๐ Windows note: Node's chmod on win32 is best-effort (it can only toggle the
|
|
26
|
+
* read-only attribute, NTFS ACLs still govern real access). We still try,
|
|
27
|
+
* and the manual hint points at icacls for a real fix.
|
|
23
28
|
*
|
|
24
29
|
* @functions
|
|
25
|
-
* โ checkConfigSecurity()
|
|
26
|
-
* โ
|
|
27
|
-
* โ
|
|
28
|
-
* โ
|
|
29
|
-
* โ
|
|
30
|
+
* โ checkConfigSecurity() - Async main security check; awaited before the TUI starts
|
|
31
|
+
* โ resolveSecurityAction() - Pure gate deciding auto-fix / prompt / warn-only (in utils.js)
|
|
32
|
+
* โ getConfigPermissions() - Returns file mode object for config
|
|
33
|
+
* โ isConfigSecure() - Boolean check if permissions are correct
|
|
34
|
+
* โ fixConfigPermissions() - Applies chmod 600 to config file (best-effort on Windows)
|
|
35
|
+
* โ promptSecurityFix() - Interactive prompt asking user to fix permissions
|
|
30
36
|
*
|
|
31
|
-
* @exports checkConfigSecurity, isConfigSecure, fixConfigPermissions
|
|
37
|
+
* @exports checkConfigSecurity, isConfigSecure, fixConfigPermissions, formatMode, formatModeRwx
|
|
32
38
|
*/
|
|
33
39
|
|
|
34
40
|
import fs from 'node:fs'
|
|
@@ -36,8 +42,9 @@ import path from 'node:path'
|
|
|
36
42
|
import os from 'node:os'
|
|
37
43
|
import readline from 'node:readline'
|
|
38
44
|
import { CONFIG_PATH } from './config.js'
|
|
45
|
+
import { resolveSecurityAction } from './utils.js'
|
|
39
46
|
|
|
40
|
-
// ๐ Config file path
|
|
47
|
+
// ๐ Config file path - matches the path used in config.js (honours the
|
|
41
48
|
// ๐ --config-dir / FCM_CONFIG_DIR override when set).
|
|
42
49
|
function getConfigPath() {
|
|
43
50
|
return CONFIG_PATH
|
|
@@ -47,6 +54,10 @@ function getConfigPath() {
|
|
|
47
54
|
// ๐ This means: owner can read+write, group and others have no permissions
|
|
48
55
|
const SECURE_MODE = 0o600
|
|
49
56
|
|
|
57
|
+
// ๐ True on Windows, where chmod is best-effort (read-only bit only) and the
|
|
58
|
+
// ๐ manual fix hint should point at icacls instead of chmod.
|
|
59
|
+
const IS_WINDOWS = process.platform === 'win32'
|
|
60
|
+
|
|
50
61
|
// ๐ Get file stats including permissions for the config file
|
|
51
62
|
// ๐ Returns null if file doesn't exist
|
|
52
63
|
function getConfigPermissions() {
|
|
@@ -80,7 +91,7 @@ export function isConfigSecure() {
|
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
// ๐ Fix config file permissions to secure mode (chmod 600)
|
|
83
|
-
// ๐
|
|
94
|
+
// ๐ Best-effort on Windows (read-only bit); returns true if successful, false otherwise
|
|
84
95
|
export function fixConfigPermissions() {
|
|
85
96
|
const configPath = getConfigPath()
|
|
86
97
|
|
|
@@ -97,23 +108,21 @@ export function fixConfigPermissions() {
|
|
|
97
108
|
}
|
|
98
109
|
|
|
99
110
|
// ๐ Format permission mode in octal (e.g., 0o644 โ "644")
|
|
100
|
-
|
|
111
|
+
// ๐ Exported for unit tests
|
|
112
|
+
export function formatMode(mode) {
|
|
101
113
|
return (mode & 0o777).toString(8).padStart(3, '0')
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
// ๐ Format permission mode in human-readable rwx format (e.g., 0o644 โ "rw-r--r--")
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
// ๐ Walks bits 8..0 in groups of three: owner rwx, group rwx, others rwx.
|
|
118
|
+
// ๐ Bit 8 = owner read, bit 7 = owner write, bit 6 = owner exec, and so on.
|
|
119
|
+
// ๐ Exported for unit tests
|
|
120
|
+
export function formatModeRwx(mode) {
|
|
107
121
|
const types = ['r', 'w', 'x']
|
|
122
|
+
const perms = []
|
|
108
123
|
|
|
109
|
-
for (let i =
|
|
110
|
-
|
|
111
|
-
if (mode & (1 << (i + j))) {
|
|
112
|
-
perms.push(types[j])
|
|
113
|
-
} else {
|
|
114
|
-
perms.push('-')
|
|
115
|
-
}
|
|
116
|
-
}
|
|
124
|
+
for (let i = 8; i >= 0; i--) {
|
|
125
|
+
perms.push(mode & (1 << i) ? types[(8 - i) % 3] : '-')
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
return [
|
|
@@ -123,10 +132,72 @@ function formatModeRwx(mode) {
|
|
|
123
132
|
].join(' / ')
|
|
124
133
|
}
|
|
125
134
|
|
|
126
|
-
// ๐
|
|
127
|
-
|
|
135
|
+
// ๐ Print the insecure-permissions warning (stderr, so --json stdout stays clean)
|
|
136
|
+
function printSecurityWarning(perms) {
|
|
137
|
+
const currentMode = formatMode(perms.mode)
|
|
138
|
+
const currentRwx = formatModeRwx(perms.mode)
|
|
139
|
+
|
|
140
|
+
console.error('')
|
|
141
|
+
console.error('โ ๏ธ SECURITY WARNING โ ๏ธ')
|
|
142
|
+
console.error('')
|
|
143
|
+
console.error(`Your config file has insecure permissions: ${currentMode} (${currentRwx})`)
|
|
144
|
+
console.error(`File: ${perms.path}`)
|
|
145
|
+
console.error('')
|
|
146
|
+
console.error('This means other users on this system may be able to read your API keys.')
|
|
147
|
+
console.error('')
|
|
148
|
+
console.error('Recommended: Fix permissions to 600 (rw-------) - owner read/write only')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ๐ Print the manual fix hint. On Windows, point at icacls since Node's chmod
|
|
152
|
+
// ๐ only toggles the read-only attribute there.
|
|
153
|
+
function printManualFixHint() {
|
|
154
|
+
console.error('')
|
|
155
|
+
if (IS_WINDOWS) {
|
|
156
|
+
console.error('To fix manually (PowerShell), run:')
|
|
157
|
+
console.error(` icacls "${getConfigPath()}" /inheritance:r /grant:r "$env:USERNAME:R,W"`)
|
|
158
|
+
} else {
|
|
159
|
+
console.error('To fix manually, run:')
|
|
160
|
+
console.error(` chmod 600 ${getConfigPath()}`)
|
|
161
|
+
}
|
|
162
|
+
console.error('')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ๐ Apply the fix and report the outcome. Shared by the prompt path (user said
|
|
166
|
+
// ๐ yes) and the auto-fix path (--fix-permissions / --yes / -y).
|
|
167
|
+
function applyFixAndReport() {
|
|
168
|
+
const success = fixConfigPermissions()
|
|
169
|
+
|
|
170
|
+
if (success) {
|
|
171
|
+
console.error('')
|
|
172
|
+
console.error('โ
Permissions fixed! Your API keys are now secure.')
|
|
173
|
+
console.error('')
|
|
174
|
+
if (IS_WINDOWS) {
|
|
175
|
+
console.error('Note: on Windows this is best-effort (read-only bit). See docs for NTFS ACLs.')
|
|
176
|
+
console.error('')
|
|
177
|
+
}
|
|
178
|
+
return { wasSecure: false, wasFixed: true }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.error('')
|
|
182
|
+
console.error('โ Failed to fix permissions automatically.')
|
|
183
|
+
printManualFixHint()
|
|
184
|
+
return { wasSecure: false, wasFixed: false, error: 'chmod_failed' }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ๐ Check security and handle the fix flow if needed
|
|
188
|
+
// ๐ Await this BEFORE starting any terminal UI (issue #173) so the warning and
|
|
189
|
+
// ๐ the confirmation prompt are visible and fully resolved before raw mode /
|
|
190
|
+
// ๐ the alternate screen take over.
|
|
191
|
+
//
|
|
192
|
+
// ๐ Options:
|
|
193
|
+
// autoFix - true when --fix-permissions / --yes / -y was passed: apply the
|
|
194
|
+
// fix without asking
|
|
195
|
+
// promptAllowed - false on daemon/web/JSON surfaces: never prompt there, at most
|
|
196
|
+
// warn on stderr
|
|
197
|
+
// stdinIsTTY - override the stdin TTY detection (tests); defaults to real detection
|
|
198
|
+
//
|
|
128
199
|
// ๐ Returns: { wasSecure: boolean, wasFixed: boolean, error?: string }
|
|
129
|
-
export function checkConfigSecurity() {
|
|
200
|
+
export async function checkConfigSecurity(options = {}) {
|
|
130
201
|
const perms = getConfigPermissions()
|
|
131
202
|
|
|
132
203
|
// ๐ No file yet = nothing to check
|
|
@@ -139,24 +210,38 @@ export function checkConfigSecurity() {
|
|
|
139
210
|
return { wasSecure: true, wasFixed: false }
|
|
140
211
|
}
|
|
141
212
|
|
|
142
|
-
// ๐
|
|
143
|
-
const
|
|
144
|
-
|
|
213
|
+
// ๐ Pure gate (see utils.js): decides auto-fix vs prompt vs warn-only.
|
|
214
|
+
const action = resolveSecurityAction({
|
|
215
|
+
configExists: true,
|
|
216
|
+
isSecure: false,
|
|
217
|
+
autoFixRequested: options.autoFix === true,
|
|
218
|
+
stdinIsTTY: options.stdinIsTTY ?? (process.stdin?.isTTY === true),
|
|
219
|
+
promptAllowed: options.promptAllowed !== false,
|
|
220
|
+
})
|
|
145
221
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
222
|
+
if (action === 'none') {
|
|
223
|
+
return { wasSecure: true, wasFixed: false }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ๐ Security issue detected! Print the warning first so it is on screen
|
|
227
|
+
// ๐ no matter which path follows.
|
|
228
|
+
printSecurityWarning(perms)
|
|
229
|
+
|
|
230
|
+
if (action === 'auto-fix') {
|
|
231
|
+
return applyFixAndReport()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (action === 'warn-only') {
|
|
235
|
+
console.error('Running non-interactively (piped stdin or daemon/web mode), so skipping the prompt.')
|
|
236
|
+
printManualFixHint()
|
|
237
|
+
return { wasSecure: false, wasFixed: false, error: 'non_interactive' }
|
|
238
|
+
}
|
|
155
239
|
|
|
156
240
|
return promptSecurityFix()
|
|
157
241
|
}
|
|
158
242
|
|
|
159
243
|
// ๐ Interactive prompt asking user if they want to auto-fix
|
|
244
|
+
// ๐ Only reached on a real interactive TTY (gated in checkConfigSecurity)
|
|
160
245
|
// ๐ Returns: { wasSecure: boolean, wasFixed: boolean, error?: string }
|
|
161
246
|
async function promptSecurityFix() {
|
|
162
247
|
const rl = readline.createInterface({
|
|
@@ -165,37 +250,22 @@ async function promptSecurityFix() {
|
|
|
165
250
|
})
|
|
166
251
|
|
|
167
252
|
try {
|
|
168
|
-
const
|
|
253
|
+
const rawAnswer = await new Promise((resolve) => {
|
|
169
254
|
rl.question('Fix permissions automatically? (Y/n): ', resolve)
|
|
170
255
|
})
|
|
171
256
|
|
|
172
257
|
rl.close()
|
|
173
258
|
|
|
259
|
+
// ๐ Normalise: readline can resolve with undefined when stdin closes mid-prompt
|
|
260
|
+
const answer = String(rawAnswer ?? '').trim().toLowerCase()
|
|
261
|
+
|
|
174
262
|
// ๐ Default to yes if user just presses Enter
|
|
175
|
-
if (answer
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (success) {
|
|
179
|
-
console.error('')
|
|
180
|
-
console.error('โ
Permissions fixed! Your API keys are now secure.')
|
|
181
|
-
console.error('')
|
|
182
|
-
return { wasSecure: false, wasFixed: true }
|
|
183
|
-
} else {
|
|
184
|
-
console.error('')
|
|
185
|
-
console.error('โ Failed to fix permissions automatically.')
|
|
186
|
-
console.error('')
|
|
187
|
-
console.error('Run this command manually:')
|
|
188
|
-
console.error(` chmod 600 ${getConfigPath()}`)
|
|
189
|
-
console.error('')
|
|
190
|
-
return { wasSecure: false, wasFixed: false, error: 'chmod_failed' }
|
|
191
|
-
}
|
|
263
|
+
if (answer === 'y' || answer === '') {
|
|
264
|
+
return applyFixAndReport()
|
|
192
265
|
} else {
|
|
193
266
|
console.error('')
|
|
194
267
|
console.error('โ ๏ธ Permissions not fixed. Your API keys may be at risk.')
|
|
195
|
-
|
|
196
|
-
console.error('To fix later, run:')
|
|
197
|
-
console.error(` chmod 600 ${getConfigPath()}`)
|
|
198
|
-
console.error('')
|
|
268
|
+
printManualFixHint()
|
|
199
269
|
return { wasSecure: false, wasFixed: false, error: 'user_declined' }
|
|
200
270
|
}
|
|
201
271
|
} catch (err) {
|
|
@@ -203,10 +273,7 @@ async function promptSecurityFix() {
|
|
|
203
273
|
// ๐ If we can't prompt (e.g., non-interactive TTY), just warn and continue
|
|
204
274
|
console.error('')
|
|
205
275
|
console.error('โ ๏ธ Unable to prompt for permission fix (non-interactive terminal?)')
|
|
206
|
-
|
|
207
|
-
console.error('To fix manually, run:')
|
|
208
|
-
console.error(` chmod 600 ${getConfigPath()}`)
|
|
209
|
-
console.error('')
|
|
276
|
+
printManualFixHint()
|
|
210
277
|
return { wasSecure: false, wasFixed: false, error: 'no_tty' }
|
|
211
278
|
}
|
|
212
279
|
}
|
package/src/core/sync-set.js
CHANGED
|
@@ -192,7 +192,7 @@ function buildHeaders(providerKey, apiKey) {
|
|
|
192
192
|
'Content-Type': 'application/json',
|
|
193
193
|
Authorization: `Bearer ${apiKey}`,
|
|
194
194
|
}
|
|
195
|
-
if (providerKey === 'openrouter') {
|
|
195
|
+
if (providerKey === 'openrouter' || providerKey === 'orcarouter') {
|
|
196
196
|
headers['HTTP-Referer'] = 'https://github.com/vava-nessa/free-coding-models'
|
|
197
197
|
headers['X-Title'] = 'free-coding-models'
|
|
198
198
|
}
|
|
@@ -50,6 +50,7 @@ import { PROVIDER_METADATA } from './provider-metadata.js'
|
|
|
50
50
|
import { resolveToolBinaryPath } from './tool-bootstrap.js'
|
|
51
51
|
import { ensureDir, readJson, writeJson } from './shared-helpers.js'
|
|
52
52
|
import { parseContextWindow } from './endpoint-installer.js'
|
|
53
|
+
import { resolveCloudflareUrl } from './ping.js'
|
|
53
54
|
|
|
54
55
|
const OPENAI_COMPAT_ENV_KEYS = [
|
|
55
56
|
'OPENAI_API_KEY',
|
|
@@ -108,10 +109,15 @@ function backupIfExists(filePath) {
|
|
|
108
109
|
function getProviderBaseUrl(providerKey) {
|
|
109
110
|
const url = sources[providerKey]?.url
|
|
110
111
|
if (!url) return null
|
|
111
|
-
|
|
112
|
+
let resolvedUrl = url
|
|
112
113
|
.replace(/\/chat\/completions$/i, '')
|
|
113
114
|
.replace(/\/responses$/i, '')
|
|
114
115
|
.replace(/\/predictions$/i, '')
|
|
116
|
+
// ๐ Cloudflare uses account_id in URL - resolve from CLOUDFLARE_ACCOUNT_ID env var
|
|
117
|
+
if (providerKey === 'cloudflare') {
|
|
118
|
+
resolvedUrl = resolveCloudflareUrl(resolvedUrl)
|
|
119
|
+
}
|
|
120
|
+
return resolvedUrl
|
|
115
121
|
}
|
|
116
122
|
|
|
117
123
|
function deleteEnvKeys(env, keys) {
|
|
@@ -579,9 +585,9 @@ export function writeZCodeConfig(model, config, paths = getDefaultToolPaths()) {
|
|
|
579
585
|
|
|
580
586
|
const providerKey = model.providerKey || 'nvidia'
|
|
581
587
|
const apiKey = getApiKey(config, providerKey)
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
588
|
+
// ๐ Use getProviderBaseUrl so Cloudflare's {account_id} placeholder is resolved
|
|
589
|
+
// ๐ (raw sources[].url would leave it literal and ZCode would fail on Cloudflare models).
|
|
590
|
+
const baseUrl = getProviderBaseUrl(providerKey)
|
|
585
591
|
const providerId = `fcm-${providerKey}`
|
|
586
592
|
const providerLabel = `FCM ${sources[providerKey]?.name || providerKey}`
|
|
587
593
|
|
|
@@ -838,7 +844,11 @@ export function prepareExternalToolLaunch(mode, model, config, options = {}) {
|
|
|
838
844
|
}
|
|
839
845
|
|
|
840
846
|
if (mode === 'goose') {
|
|
841
|
-
|
|
847
|
+
// ๐ Always use the resolved provider base URL from buildToolEnv (getProviderBaseUrl),
|
|
848
|
+
// ๐ which substitutes Cloudflare's {account_id} placeholder via resolveCloudflareUrl.
|
|
849
|
+
// ๐ Using the raw sources[].url would leave the literal {account_id} in the Goose
|
|
850
|
+
// ๐ provider config, so Goose would POST to /accounts/{account_id}/... and 400.
|
|
851
|
+
const gooseBaseUrl = baseUrl
|
|
842
852
|
const gooseModelId = resolveLauncherModelId(model)
|
|
843
853
|
const result = writeGooseConfig({ ...model, modelId: gooseModelId }, apiKey, gooseBaseUrl, model.providerKey, paths)
|
|
844
854
|
env.GOOSE_PROVIDER = `fcm-${model.providerKey}`
|
package/src/core/utils.js
CHANGED
|
@@ -409,6 +409,30 @@ export function filterByTier(results, tierLetter) {
|
|
|
409
409
|
return results.filter(r => allowed.includes(r.tier))
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
// ๐ PROBE_FAILED_STATUSES: row health states that count as "failed" for the
|
|
413
|
+
// ๐ Shift+P re-probe action (issue #168). Covers the errors a user actually sees
|
|
414
|
+
// ๐ in the table: dead endpoints (404/410/5xx/ERR โ 'down'), rate limits
|
|
415
|
+
// ๐ (429 โ 'down'), rejected keys ('auth_error') and network timeouts.
|
|
416
|
+
// ๐ 'noauth' is deliberately excluded: those rows have no API key, so an
|
|
417
|
+
// ๐ authenticated probe cannot even run for them.
|
|
418
|
+
export const PROBE_FAILED_STATUSES = new Set(['down', 'timeout', 'auth_error'])
|
|
419
|
+
|
|
420
|
+
// ๐ isProbeFailedRow: True when a single result row is currently showing an
|
|
421
|
+
// ๐ error state worth re-probing. Hidden rows are skipped on purpose: Shift+P
|
|
422
|
+
// ๐ re-probes what the user can SEE failing, not rows the probe already hid.
|
|
423
|
+
export function isProbeFailedRow(row) {
|
|
424
|
+
return !!row && !row.hidden && PROBE_FAILED_STATUSES.has(row.status)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ๐ selectProbeFailedRows: Filter a results array down to the rows that are
|
|
428
|
+
// ๐ currently failing (auth fail / 429 / 404 / timeout). Pure helper used by
|
|
429
|
+
// ๐ the TUI Shift+P "re-probe failed rows only" action (issue #168) so users
|
|
430
|
+
// ๐ can retry 1-2 flaky providers without burning quota on the whole list.
|
|
431
|
+
export function selectProbeFailedRows(results) {
|
|
432
|
+
if (!Array.isArray(results)) return []
|
|
433
|
+
return results.filter(isProbeFailedRow)
|
|
434
|
+
}
|
|
435
|
+
|
|
412
436
|
// ๐ findBestModel: Pick the single best model from a results array.
|
|
413
437
|
// ๐ Used by --fiable mode to output the most reliable model after 10s of analysis.
|
|
414
438
|
//
|
|
@@ -460,6 +484,7 @@ export function findBestModel(results) {
|
|
|
460
484
|
// --daemon, --daemon-bg, --daemon-stop,
|
|
461
485
|
// --daemon-status, --no-telemetry, --json, --help/-h (case-insensitive)
|
|
462
486
|
// --playground / playground subcommand (open the in-TUI chat playground)
|
|
487
|
+
// --fix-permissions / --yes / -y (auto-fix config permissions without prompting)
|
|
463
488
|
// - Value flag: --tier <letter> (the next non-flag arg is the tier value)
|
|
464
489
|
// - Probe-cache flags (t1):
|
|
465
490
|
// --reprobe / --no-cache (boolean) โ force-rebuild the probe cache this run
|
|
@@ -533,7 +558,8 @@ export function parseArgs(argv) {
|
|
|
533
558
|
if (configDirValueIdx !== -1) skipIndices.add(configDirValueIdx)
|
|
534
559
|
|
|
535
560
|
for (const [i, arg] of args.entries()) {
|
|
536
|
-
|
|
561
|
+
// ๐ -y is a boolean flag (security auto-fix), never an API key
|
|
562
|
+
if (arg.startsWith('--') || arg === '-h' || arg === '-y') {
|
|
537
563
|
flags.push(arg.toLowerCase())
|
|
538
564
|
} else if (skipIndices.has(i)) {
|
|
539
565
|
// ๐ Skip โ this is a value for --tier, not an API key
|
|
@@ -577,6 +603,11 @@ export function parseArgs(argv) {
|
|
|
577
603
|
const daemonStopMode = flags.includes('--daemon-stop')
|
|
578
604
|
const daemonStatusMode = flags.includes('--daemon-status')
|
|
579
605
|
|
|
606
|
+
// ๐ --fix-permissions / --yes / -y - auto-answer "yes" to the config-permission
|
|
607
|
+
// ๐ security prompt (chmod 600, best-effort on Windows) so scripts, CI and
|
|
608
|
+
// ๐ non-interactive terminals never hang on a hidden prompt. Issue #173.
|
|
609
|
+
const fixPermissionsMode = flags.includes('--fix-permissions') || flags.includes('--yes') || flags.includes('-y')
|
|
610
|
+
|
|
580
611
|
// ๐ --sync-set [name] โ auto-discover and populate a router set with best available models
|
|
581
612
|
const syncSetMode = flags.includes('--sync-set')
|
|
582
613
|
const syncSetName = syncSetValueIdx !== -1 ? args[syncSetValueIdx] : null
|
|
@@ -686,9 +717,65 @@ export function parseArgs(argv) {
|
|
|
686
717
|
clearRuntimeMode,
|
|
687
718
|
// ๐ Config location flag โ see src/core/config.js getConfigDir()
|
|
688
719
|
configDir: configDirValueIdx !== -1 ? args[configDirValueIdx] : null,
|
|
720
|
+
// ๐ Security auto-fix flag - see src/core/security.js checkConfigSecurity()
|
|
721
|
+
fixPermissionsMode,
|
|
689
722
|
}
|
|
690
723
|
}
|
|
691
724
|
|
|
725
|
+
// โโโ Config Security Gating (issue #173) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
726
|
+
|
|
727
|
+
// ๐ resolveSecurityAction: pure decision helper that tells the startup security
|
|
728
|
+
// ๐ check what it should do about insecure config file permissions.
|
|
729
|
+
//
|
|
730
|
+
// ๐ Why: the security warning + "Fix permissions automatically?" prompt used to
|
|
731
|
+
// ๐ run un-awaited while the TUI entered raw mode / the alternate screen, so the
|
|
732
|
+
// ๐ prompt was invisible (worst on Windows) and the app looked frozen. This gate
|
|
733
|
+
// ๐ guarantees: never prompt without an interactive surface, never prompt a
|
|
734
|
+
// ๐ daemon/web/JSON surface, and always auto-fix when a yes-flag is passed.
|
|
735
|
+
//
|
|
736
|
+
// ๐ Params:
|
|
737
|
+
// configExists - does the config file exist (no file = nothing to secure)
|
|
738
|
+
// isSecure - are permissions already 0600
|
|
739
|
+
// autoFixRequested - user passed --fix-permissions / --yes / -y
|
|
740
|
+
// stdinIsTTY - is stdin an interactive terminal
|
|
741
|
+
// promptAllowed - is this an interactive surface (TUI)? false for daemon/web/JSON
|
|
742
|
+
//
|
|
743
|
+
// ๐ Returns one of: 'none' | 'auto-fix' | 'warn-only' | 'prompt'
|
|
744
|
+
export function resolveSecurityAction({ configExists, isSecure, autoFixRequested, stdinIsTTY, promptAllowed }) {
|
|
745
|
+
if (!configExists || isSecure) return 'none'
|
|
746
|
+
if (autoFixRequested) return 'auto-fix'
|
|
747
|
+
if (!stdinIsTTY || !promptAllowed) return 'warn-only'
|
|
748
|
+
return 'prompt'
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// ๐ detectTerminalCapabilities: PURE terminal capability probe for TUI overlays.
|
|
752
|
+
// ๐ WHY: basic server consoles (IPMI/KVM viewers, ASPEED framebuffer, serial
|
|
753
|
+
// ๐ terminals) often run 80x24 or smaller with no or limited color support, and
|
|
754
|
+
// ๐ the palette/overlays must degrade instead of overflowing or painting garbage.
|
|
755
|
+
// ๐ Everything is injected by the caller (env, size, TTY) so tests never touch
|
|
756
|
+
// ๐ process.env and the function stays deterministic.
|
|
757
|
+
//
|
|
758
|
+
// ๐ Rules:
|
|
759
|
+
// - FORCE_COLOR wins over NO_COLOR (same precedence chalk uses).
|
|
760
|
+
// - NO_COLOR (non-empty, per no-color.org spec) disables color.
|
|
761
|
+
// - Not a TTY, TERM missing / "dumb" / "unknown" disables color, unless
|
|
762
|
+
// COLORTERM is set (some terminals export only COLORTERM).
|
|
763
|
+
// - compact = size too tight for the roomy overlay layout (cols < 90 or rows < 24,
|
|
764
|
+
// thresholds chosen so a plain 80x24 console gets the space-saving layout).
|
|
765
|
+
//
|
|
766
|
+
// ๐ Returns { colorSupported: boolean, compact: boolean }
|
|
767
|
+
export function detectTerminalCapabilities({ env = {}, cols = 80, rows = 24, isTTY = true } = {}) {
|
|
768
|
+
const term = String(env.TERM ?? '').trim().toLowerCase()
|
|
769
|
+
const colorterm = String(env.COLORTERM ?? '').trim().toLowerCase()
|
|
770
|
+
const force = env.FORCE_COLOR
|
|
771
|
+
const forceOn = force !== undefined && force !== '' && force !== '0' && force !== 'false'
|
|
772
|
+
const noColor = env.NO_COLOR !== undefined && env.NO_COLOR !== ''
|
|
773
|
+
const termUsable = term !== '' && term !== 'dumb' && term !== 'unknown'
|
|
774
|
+
const colorSupported = forceOn || (!noColor && isTTY !== false && (termUsable || colorterm !== ''))
|
|
775
|
+
const compact = Math.floor(cols) < 90 || Math.floor(rows) < 24
|
|
776
|
+
return { colorSupported, compact }
|
|
777
|
+
}
|
|
778
|
+
|
|
692
779
|
// โโโ Smart Recommend โ Scoring Engine โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
693
780
|
|
|
694
781
|
// ๐ Task types for the Smart Recommend questionnaire.
|
package/src/tui/app.js
CHANGED
|
@@ -143,7 +143,6 @@ import {
|
|
|
143
143
|
isProviderQuotaPaused,
|
|
144
144
|
listPausedProviders,
|
|
145
145
|
} from '../core/provider-cooldown.js'
|
|
146
|
-
import { checkConfigSecurity } from '../core/security.js'
|
|
147
146
|
import { buildCliHelpText } from './cli-help.js'
|
|
148
147
|
import { detectActiveTheme, THEME_BG_RGB, getTheme, patchThemeBg } from './theme.js'
|
|
149
148
|
|
|
@@ -271,10 +270,9 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
271
270
|
detectActiveTheme(config.settings?.theme || 'auto')
|
|
272
271
|
|
|
273
272
|
// ๐ Check config file security โ warn and offer auto-fix if permissions are too open
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
273
|
+
// ๐ (issue #173): this moved to bin/free-coding-models.js BEFORE runApp so the
|
|
274
|
+
// ๐ warning + prompt are visible and resolved before the alternate screen / raw
|
|
275
|
+
// ๐ mode take over. Do not call checkConfigSecurity() here again.
|
|
278
276
|
|
|
279
277
|
// ๐ Apply CLI overrides for settings
|
|
280
278
|
if (cliArgs.sortColumn) config.settings.sortColumn = cliArgs.sortColumn
|
|
@@ -1045,6 +1043,10 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
1045
1043
|
probeTotal: state.probeTotal,
|
|
1046
1044
|
probeCompleted: state.probeCompleted,
|
|
1047
1045
|
probeHiddenCount: state.probeHiddenCount,
|
|
1046
|
+
// ๐ Space-expanded row (issue #168): "providerKey/modelId" or null
|
|
1047
|
+
expandedRowKey: state.expandedRowKey ?? null,
|
|
1048
|
+
// ๐ Non-fatal action error chip (issue #168): only shown while fresh
|
|
1049
|
+
actionErrorMsg: (state.actionErrorMsgUntil || 0) > Date.now() ? state.actionErrorMsg : null,
|
|
1048
1050
|
// ๐ Probe-cache (t1): footer chip stats
|
|
1049
1051
|
probeCacheHits: state.probeCacheHits || 0,
|
|
1050
1052
|
probeCacheMisses: state.probeCacheMisses || 0,
|
package/src/tui/cli-help.js
CHANGED
|
@@ -49,6 +49,7 @@ const CONFIG_FLAGS = [
|
|
|
49
49
|
{ flag: '--daemon-stop', description: 'Gracefully stop the FCM Router daemon' },
|
|
50
50
|
{ flag: '--sync-set [name]', description: 'Auto-discover and live-probe models into a router set' },
|
|
51
51
|
{ flag: '--config-dir <dir>', description: 'Store config.json + backups/ in <dir> (e.g. --config-dir ~/.config/free-coding-models for the XDG layout)' },
|
|
52
|
+
{ flag: '--fix-permissions, --yes, -y', description: 'Auto-fix insecure config file permissions (chmod 600) without prompting; safe for scripts and CI' },
|
|
52
53
|
{ flag: '--no-telemetry', description: 'Disable anonymous telemetry for this run' },
|
|
53
54
|
{ flag: '--help, -h', description: 'Print this help and exit' },
|
|
54
55
|
]
|
|
@@ -45,7 +45,7 @@ const TOOL_MODE_DESCRIPTIONS = {
|
|
|
45
45
|
hermes: 'Launch Hermes Agent with the selected model.',
|
|
46
46
|
'continue': 'Launch Continue CLI with the selected model.',
|
|
47
47
|
cline: 'Launch Cline CLI with the selected model.',
|
|
48
|
-
caveman: 'Caveman Code
|
|
48
|
+
caveman: 'Caveman Code - token-efficient coding agent (launch with Caveman tool only).',
|
|
49
49
|
jcode: 'Launch jcode coding agent with the selected model.',
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -201,7 +201,8 @@ const BASE_COMMAND_TREE = [
|
|
|
201
201
|
},
|
|
202
202
|
{ id: 'action-cycle-theme', label: 'Cycle theme', shortcut: 'G', icon: '๐', description: 'Switch dark/light/auto', keywords: ['theme', 'dark', 'light', 'auto'] },
|
|
203
203
|
{ id: 'action-reset-view', label: 'Reset view', shortcut: 'N', icon: '\u{1F504}', description: 'Reset filters and sort', keywords: ['reset', 'view', 'sort', 'filters'] },
|
|
204
|
-
{ id: 'action-probe-
|
|
204
|
+
{ id: 'action-probe-failed', label: 'Re-probe Failed Models', shortcut: 'Shift+P', icon: '\u{1F50D}', description: 'Re-test only rows showing errors (auth fail / 429 / 404 / timeout) - not the whole list.', keywords: ['probe', 'failed', 'retry', '429', 'auth', 'error', 'broken', 'rescan', 'health'] },
|
|
205
|
+
{ id: 'action-probe-404', label: 'Probe All Models (404)', shortcut: 'Ctrl+Shift+P', icon: '\u{1F50D}', description: 'Test all configured models for 404/410 errors. Auto-hides broken models.', keywords: ['probe', '404', 'broken', 'dead', 'health', 'check', 'test', 'verify', 'all'] },
|
|
205
206
|
{ id: 'action-toggle-auto-hide-broken', label: 'Toggle Auto-hide Broken Models', icon: '\u{1F6A9}', description: 'Enable/disable auto-hiding of 404/410 models. Unhides all when disabled.', keywords: ['auto', 'hide', 'broken', '404', 'models', 'toggle', 'probe'] },
|
|
206
207
|
],
|
|
207
208
|
},
|