free-coding-models 0.3.9 → 0.3.11
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 +21 -0
- package/bin/free-coding-models.js +31 -31
- package/package.json +1 -1
- package/src/cli-help.js +1 -1
- package/src/config.js +18 -240
- package/src/error-classifier.js +4 -1
- package/src/favorites.js +0 -14
- package/src/key-handler.js +26 -212
- package/src/overlays.js +4 -34
- package/src/proxy-foreground.js +234 -0
- package/src/proxy-server.js +41 -12
- package/src/proxy-sync.js +28 -2
- package/src/render-table.js +6 -17
- package/src/utils.js +7 -11
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/proxy-foreground.js
|
|
3
|
+
* @description Foreground proxy mode — starts the FCM Proxy V2 in the current terminal
|
|
4
|
+
* with a live dashboard showing status, accounts, and incoming requests.
|
|
5
|
+
*
|
|
6
|
+
* 📖 This is the `--proxy` flag handler. Unlike the daemon, it runs in the foreground
|
|
7
|
+
* with a live-updating terminal UI that shows proxy health and request activity.
|
|
8
|
+
* Perfect for debugging, dev testing (no .git check), and monitoring.
|
|
9
|
+
*
|
|
10
|
+
* @functions
|
|
11
|
+
* → startForegroundProxy(config, chalk) — main entry point, starts proxy + dashboard
|
|
12
|
+
*
|
|
13
|
+
* @exports startForegroundProxy
|
|
14
|
+
*
|
|
15
|
+
* @see src/proxy-server.js — ProxyServer implementation
|
|
16
|
+
* @see src/proxy-topology.js — topology builder
|
|
17
|
+
* @see bin/fcm-proxy-daemon.js — headless daemon equivalent
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { loadConfig, getProxySettings } from './config.js'
|
|
21
|
+
import { ProxyServer } from './proxy-server.js'
|
|
22
|
+
import { buildProxyTopologyFromConfig, buildMergedModelsForDaemon } from './proxy-topology.js'
|
|
23
|
+
import { sources } from '../sources.js'
|
|
24
|
+
import { syncProxyToTool, resolveProxySyncToolMode } from './proxy-sync.js'
|
|
25
|
+
import { buildMergedModels } from './model-merger.js'
|
|
26
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
27
|
+
|
|
28
|
+
// 📖 Default foreground proxy port — same as daemon
|
|
29
|
+
const DEFAULT_PORT = 18045
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 📖 Start the proxy in foreground mode with a live terminal dashboard.
|
|
33
|
+
* 📖 No .git check, no daemon install — just starts the proxy and shows activity.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} config — loaded FCM config
|
|
36
|
+
* @param {object} chalk — chalk instance for terminal colors
|
|
37
|
+
*/
|
|
38
|
+
export async function startForegroundProxy(config, chalk) {
|
|
39
|
+
const proxySettings = getProxySettings(config)
|
|
40
|
+
const port = proxySettings.preferredPort || DEFAULT_PORT
|
|
41
|
+
|
|
42
|
+
// 📖 Ensure a stable token exists — generate one if missing (dev-friendly)
|
|
43
|
+
let token = proxySettings.stableToken
|
|
44
|
+
if (!token) {
|
|
45
|
+
token = 'fcm_' + randomBytes(16).toString('hex')
|
|
46
|
+
console.log(chalk.yellow(' ⚠ No stableToken in config — generated a temporary one for this session'))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log()
|
|
50
|
+
console.log(chalk.bold(' 📡 FCM Proxy V2 — Foreground Mode'))
|
|
51
|
+
console.log(chalk.dim(' ─────────────────────────────────────────────'))
|
|
52
|
+
console.log()
|
|
53
|
+
|
|
54
|
+
// 📖 Build topology
|
|
55
|
+
console.log(chalk.dim(' Building merged model catalog...'))
|
|
56
|
+
let mergedModels
|
|
57
|
+
try {
|
|
58
|
+
mergedModels = await buildMergedModelsForDaemon()
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(chalk.red(` ✗ Failed to build model catalog: ${err.message}`))
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const topology = buildProxyTopologyFromConfig(config, mergedModels, sources)
|
|
65
|
+
const { accounts, proxyModels, anthropicRouting } = topology
|
|
66
|
+
|
|
67
|
+
if (accounts.length === 0) {
|
|
68
|
+
console.error(chalk.red(' ✗ No API keys configured — no accounts to serve.'))
|
|
69
|
+
console.error(chalk.dim(' Add keys via the TUI first (run free-coding-models without --proxy)'))
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 📖 Start proxy server
|
|
74
|
+
const proxy = new ProxyServer({
|
|
75
|
+
port,
|
|
76
|
+
accounts,
|
|
77
|
+
proxyApiKey: token,
|
|
78
|
+
anthropicRouting,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
let listeningPort
|
|
82
|
+
try {
|
|
83
|
+
const result = await proxy.start()
|
|
84
|
+
listeningPort = result.port
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (err.code === 'EADDRINUSE') {
|
|
87
|
+
console.error(chalk.red(` ✗ Port ${port} already in use.`))
|
|
88
|
+
console.error(chalk.dim(' Another FCM proxy or process may be running on that port.'))
|
|
89
|
+
process.exit(2)
|
|
90
|
+
}
|
|
91
|
+
console.error(chalk.red(` ✗ Failed to start proxy: ${err.message}`))
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const modelCount = Object.keys(proxyModels).length
|
|
96
|
+
|
|
97
|
+
// 📖 Sync env file for claude-code if it's a syncable tool
|
|
98
|
+
try {
|
|
99
|
+
const baseUrl = `http://127.0.0.1:${listeningPort}/v1`
|
|
100
|
+
const proxyInfo = { baseUrl, token }
|
|
101
|
+
syncProxyToTool('claude-code', proxyInfo, mergedModels)
|
|
102
|
+
} catch { /* best effort */ }
|
|
103
|
+
|
|
104
|
+
// 📖 Dashboard header
|
|
105
|
+
console.log(chalk.green(' ✓ Proxy running'))
|
|
106
|
+
console.log()
|
|
107
|
+
console.log(chalk.bold(' Status'))
|
|
108
|
+
console.log(chalk.dim(' ─────────────────────────────────────────────'))
|
|
109
|
+
console.log(` ${chalk.cyan('Endpoint')} http://127.0.0.1:${listeningPort}`)
|
|
110
|
+
console.log(` ${chalk.cyan('Token')} ${token.slice(0, 12)}...${token.slice(-4)}`)
|
|
111
|
+
console.log(` ${chalk.cyan('Accounts')} ${accounts.length}`)
|
|
112
|
+
console.log(` ${chalk.cyan('Models')} ${modelCount}`)
|
|
113
|
+
console.log()
|
|
114
|
+
|
|
115
|
+
// 📖 Show provider breakdown
|
|
116
|
+
const byProvider = {}
|
|
117
|
+
for (const acct of accounts) {
|
|
118
|
+
byProvider[acct.providerKey] = (byProvider[acct.providerKey] || 0) + 1
|
|
119
|
+
}
|
|
120
|
+
console.log(chalk.bold(' Providers'))
|
|
121
|
+
console.log(chalk.dim(' ─────────────────────────────────────────────'))
|
|
122
|
+
for (const [provider, count] of Object.entries(byProvider).sort((a, b) => b[1] - a[1])) {
|
|
123
|
+
console.log(` ${chalk.cyan(provider.padEnd(20))} ${count} account${count > 1 ? 's' : ''}`)
|
|
124
|
+
}
|
|
125
|
+
console.log()
|
|
126
|
+
|
|
127
|
+
// 📖 Claude Code quick-start hint
|
|
128
|
+
console.log(chalk.bold(' Quick Start'))
|
|
129
|
+
console.log(chalk.dim(' ─────────────────────────────────────────────'))
|
|
130
|
+
console.log(chalk.dim(' Claude Code:'))
|
|
131
|
+
console.log(` ${chalk.cyan(`ANTHROPIC_BASE_URL=http://127.0.0.1:${listeningPort} ANTHROPIC_API_KEY=${token} claude`)}`)
|
|
132
|
+
console.log()
|
|
133
|
+
console.log(chalk.dim(' curl test:'))
|
|
134
|
+
console.log(` ${chalk.cyan(`curl -s -H "x-api-key: ${token}" http://127.0.0.1:${listeningPort}/v1/models | head`)}`)
|
|
135
|
+
console.log()
|
|
136
|
+
|
|
137
|
+
console.log(chalk.bold(' Live Requests'))
|
|
138
|
+
console.log(chalk.dim(' ─────────────────────────────────────────────'))
|
|
139
|
+
console.log(chalk.dim(' Waiting for incoming requests... (Ctrl+C to stop)'))
|
|
140
|
+
console.log()
|
|
141
|
+
|
|
142
|
+
// 📖 Monkey-patch tokenStats.record to intercept and display live requests
|
|
143
|
+
const originalRecord = proxy._tokenStats.record.bind(proxy._tokenStats)
|
|
144
|
+
let requestCount = 0
|
|
145
|
+
proxy._tokenStats.record = (entry) => {
|
|
146
|
+
originalRecord(entry)
|
|
147
|
+
requestCount++
|
|
148
|
+
|
|
149
|
+
const now = new Date().toLocaleTimeString()
|
|
150
|
+
const status = entry.success ? chalk.green(`${entry.statusCode}`) : chalk.red(`${entry.statusCode}`)
|
|
151
|
+
const latency = entry.latencyMs ? chalk.dim(`${entry.latencyMs}ms`) : ''
|
|
152
|
+
const tokens = (entry.promptTokens + entry.completionTokens) > 0
|
|
153
|
+
? chalk.dim(`${entry.promptTokens}+${entry.completionTokens}tok`)
|
|
154
|
+
: ''
|
|
155
|
+
const reqType = entry.requestType || 'unknown'
|
|
156
|
+
const model = entry.requestedModelId || entry.modelId || '?'
|
|
157
|
+
const provider = entry.providerKey || '?'
|
|
158
|
+
const switched = entry.switched ? chalk.yellow(' ↻') : ''
|
|
159
|
+
|
|
160
|
+
console.log(
|
|
161
|
+
` ${chalk.dim(now)} ${status} ${chalk.cyan(reqType.padEnd(20))} ` +
|
|
162
|
+
`${chalk.white(model)} → ${chalk.dim(provider)}${switched} ${latency} ${tokens}`
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 📖 Also intercept errors on the _handleRequest level to show auth failures etc.
|
|
167
|
+
const originalHandleRequest = proxy._handleRequest.bind(proxy)
|
|
168
|
+
proxy._handleRequest = (req, res) => {
|
|
169
|
+
const origEnd = res.end.bind(res)
|
|
170
|
+
const method = req.method
|
|
171
|
+
const url = req.url
|
|
172
|
+
let logged = false
|
|
173
|
+
|
|
174
|
+
res.end = function (...args) {
|
|
175
|
+
if (!logged && res.statusCode >= 400) {
|
|
176
|
+
logged = true
|
|
177
|
+
const now = new Date().toLocaleTimeString()
|
|
178
|
+
const status = chalk.red(`${res.statusCode}`)
|
|
179
|
+
const ua = req.headers['user-agent'] || ''
|
|
180
|
+
// 📖 Try to detect the client tool from user-agent
|
|
181
|
+
const tool = detectClientTool(ua, req.headers)
|
|
182
|
+
const toolLabel = tool ? chalk.magenta(` [${tool}]`) : ''
|
|
183
|
+
console.log(
|
|
184
|
+
` ${chalk.dim(now)} ${status} ${chalk.cyan(`${method} ${url}`.padEnd(20))} ` +
|
|
185
|
+
`${chalk.dim('rejected')}${toolLabel}`
|
|
186
|
+
)
|
|
187
|
+
// 📖 Debug: show auth headers on 401 to help diagnose auth issues
|
|
188
|
+
if (res.statusCode === 401) {
|
|
189
|
+
const authHeader = req.headers.authorization ? `Bearer ${req.headers.authorization.slice(0, 20)}...` : 'none'
|
|
190
|
+
const xApiKeyHeader = req.headers['x-api-key'] ? `${req.headers['x-api-key'].slice(0, 20)}...` : 'none'
|
|
191
|
+
console.log(chalk.dim(` auth: ${authHeader} | x-api-key: ${xApiKeyHeader}`))
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return origEnd(...args)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return originalHandleRequest(req, res)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 📖 Graceful shutdown
|
|
201
|
+
const shutdown = async (signal) => {
|
|
202
|
+
console.log()
|
|
203
|
+
console.log(chalk.dim(` Received ${signal} — shutting down...`))
|
|
204
|
+
try { await proxy.stop() } catch { /* best effort */ }
|
|
205
|
+
console.log(chalk.green(` ✓ Proxy stopped. ${requestCount} request${requestCount !== 1 ? 's' : ''} served this session.`))
|
|
206
|
+
console.log()
|
|
207
|
+
process.exit(0)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
process.on('SIGINT', () => shutdown('SIGINT'))
|
|
211
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 📖 Detect which client tool sent the request based on User-Agent or custom headers.
|
|
216
|
+
* 📖 Claude Code, Codex, OpenCode etc. each have distinctive UA patterns.
|
|
217
|
+
*/
|
|
218
|
+
function detectClientTool(ua, headers) {
|
|
219
|
+
if (!ua && !headers) return null
|
|
220
|
+
const uaLower = (ua || '').toLowerCase()
|
|
221
|
+
|
|
222
|
+
if (uaLower.includes('claude') || uaLower.includes('anthropic')) return 'Claude Code'
|
|
223
|
+
if (headers?.['anthropic-version'] || headers?.['x-api-key']) return 'Anthropic SDK'
|
|
224
|
+
if (uaLower.includes('codex')) return 'Codex'
|
|
225
|
+
if (uaLower.includes('opencode')) return 'OpenCode'
|
|
226
|
+
if (uaLower.includes('cursor')) return 'Cursor'
|
|
227
|
+
if (uaLower.includes('aider')) return 'Aider'
|
|
228
|
+
if (uaLower.includes('goose')) return 'Goose'
|
|
229
|
+
if (uaLower.includes('openclaw')) return 'OpenClaw'
|
|
230
|
+
if (uaLower.includes('node-fetch') || uaLower.includes('undici')) return 'Node.js'
|
|
231
|
+
if (uaLower.includes('python')) return 'Python'
|
|
232
|
+
if (uaLower.includes('curl')) return 'curl'
|
|
233
|
+
return null
|
|
234
|
+
}
|
package/src/proxy-server.js
CHANGED
|
@@ -175,20 +175,42 @@ function resolveAnthropicMappedModel(modelId, anthropicRouting) {
|
|
|
175
175
|
return fallbackModel
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
// 📖 Accepts both standard Bearer auth and Anthropic SDK x-api-key header
|
|
179
|
+
// 📖 Claude Code sends credentials via x-api-key, not Authorization: Bearer
|
|
180
|
+
function parseProxyAuthorizationHeader(authorization, expectedToken, xApiKey = null) {
|
|
179
181
|
if (!expectedToken) return { authorized: true, modelHint: null }
|
|
180
|
-
|
|
181
|
-
|
|
182
|
+
|
|
183
|
+
// 📖 Check standard Bearer auth first
|
|
184
|
+
if (typeof authorization === 'string' && authorization.startsWith('Bearer ')) {
|
|
185
|
+
const rawToken = authorization.slice('Bearer '.length).trim()
|
|
186
|
+
if (rawToken === expectedToken) return { authorized: true, modelHint: null }
|
|
187
|
+
if (rawToken.startsWith(`${expectedToken}:`)) {
|
|
188
|
+
const modelHint = normalizeRequestedModel(rawToken.slice(expectedToken.length + 1))
|
|
189
|
+
return modelHint ? { authorized: true, modelHint } : { authorized: false, modelHint: null }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 📖 Fallback: Anthropic SDK x-api-key header
|
|
194
|
+
if (typeof xApiKey === 'string' && xApiKey.trim()) {
|
|
195
|
+
const trimmed = xApiKey.trim()
|
|
196
|
+
if (trimmed === expectedToken) return { authorized: true, modelHint: null }
|
|
197
|
+
if (trimmed.startsWith(`${expectedToken}:`)) {
|
|
198
|
+
const modelHint = normalizeRequestedModel(trimmed.slice(expectedToken.length + 1))
|
|
199
|
+
return modelHint ? { authorized: true, modelHint } : { authorized: false, modelHint: null }
|
|
200
|
+
}
|
|
182
201
|
}
|
|
183
202
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
203
|
+
// 📖 Accept real Anthropic API keys (sk-ant-*) — Claude Code uses its own stored key
|
|
204
|
+
// 📖 even when ANTHROPIC_BASE_URL is overridden to point at the proxy.
|
|
205
|
+
// 📖 The proxy is bound to 127.0.0.1 only, so accepting these keys is safe.
|
|
206
|
+
const candidateToken = (typeof authorization === 'string' && authorization.startsWith('Bearer '))
|
|
207
|
+
? authorization.slice('Bearer '.length).trim()
|
|
208
|
+
: (typeof xApiKey === 'string' ? xApiKey.trim() : '')
|
|
209
|
+
if (candidateToken.startsWith('sk-ant-')) {
|
|
210
|
+
return { authorized: true, modelHint: null }
|
|
211
|
+
}
|
|
187
212
|
|
|
188
|
-
|
|
189
|
-
return modelHint
|
|
190
|
-
? { authorized: true, modelHint }
|
|
191
|
-
: { authorized: false, modelHint: null }
|
|
213
|
+
return { authorized: false, modelHint: null }
|
|
192
214
|
}
|
|
193
215
|
|
|
194
216
|
// ─── ProxyServer ─────────────────────────────────────────────────────────────
|
|
@@ -211,7 +233,7 @@ export class ProxyServer {
|
|
|
211
233
|
constructor({
|
|
212
234
|
port = 0,
|
|
213
235
|
accounts = [],
|
|
214
|
-
retries =
|
|
236
|
+
retries = 8,
|
|
215
237
|
proxyApiKey = null,
|
|
216
238
|
anthropicRouting = null,
|
|
217
239
|
accountManagerOpts = {},
|
|
@@ -284,7 +306,7 @@ export class ProxyServer {
|
|
|
284
306
|
}
|
|
285
307
|
|
|
286
308
|
_getAuthContext(req) {
|
|
287
|
-
return parseProxyAuthorizationHeader(req.headers.authorization, this._proxyApiKey)
|
|
309
|
+
return parseProxyAuthorizationHeader(req.headers.authorization, this._proxyApiKey, req.headers['x-api-key'])
|
|
288
310
|
}
|
|
289
311
|
|
|
290
312
|
_isAuthorized(req) {
|
|
@@ -312,6 +334,13 @@ export class ProxyServer {
|
|
|
312
334
|
}
|
|
313
335
|
}
|
|
314
336
|
|
|
337
|
+
// 📖 Last resort: when the requested model is a Claude virtual model and no routing resolved,
|
|
338
|
+
// 📖 fall back to the first available account's model (free-claude-code behavior)
|
|
339
|
+
if (!requestedModel || classifyClaudeVirtualModel(requestedModel) || requestedModel.toLowerCase().startsWith('claude-')) {
|
|
340
|
+
const firstModel = this._accounts[0]?.modelId
|
|
341
|
+
if (firstModel) return firstModel
|
|
342
|
+
}
|
|
343
|
+
|
|
315
344
|
return requestedModel
|
|
316
345
|
}
|
|
317
346
|
|
package/src/proxy-sync.js
CHANGED
|
@@ -32,13 +32,33 @@ import { getToolMeta } from './tool-metadata.js'
|
|
|
32
32
|
// 📖 Provider ID used for all proxy entries — replaces per-provider fcm-{providerKey} IDs
|
|
33
33
|
const PROXY_PROVIDER_ID = 'fcm-proxy'
|
|
34
34
|
|
|
35
|
+
// 📖 Ensures the env file is sourced in the user's shell profile (.zshrc, .bashrc, .bash_profile)
|
|
36
|
+
// 📖 Only adds the source line if not already present — idempotent
|
|
37
|
+
function ensureShellSourceLine(envFilePath) {
|
|
38
|
+
const home = homedir()
|
|
39
|
+
const candidates = ['.zshrc', '.bashrc', '.bash_profile']
|
|
40
|
+
let profilePath = null
|
|
41
|
+
for (const c of candidates) {
|
|
42
|
+
const p = join(home, c)
|
|
43
|
+
if (existsSync(p)) { profilePath = p; break }
|
|
44
|
+
}
|
|
45
|
+
if (!profilePath) return // 📖 No shell profile found — skip silently
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const content = readFileSync(profilePath, 'utf8')
|
|
49
|
+
if (content.includes(envFilePath)) return // 📖 Already sourced
|
|
50
|
+
const sourceLine = `\n# 📖 FCM Proxy — Claude Code env vars\n[ -f "${envFilePath}" ] && source "${envFilePath}"\n`
|
|
51
|
+
writeFileSync(profilePath, content + sourceLine)
|
|
52
|
+
} catch { /* best effort */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
35
55
|
// 📖 Tools that support proxy sync (have base URL + API key config)
|
|
36
56
|
// 📖 Gemini is excluded — it only stores a model name, no URL/key fields.
|
|
37
57
|
// 📖 Claude proxy integration is
|
|
38
58
|
// 📖 runtime-only now, with fake Claude ids handled by the proxy itself.
|
|
39
59
|
export const PROXY_SYNCABLE_TOOLS = [
|
|
40
60
|
'opencode', 'opencode-desktop', 'openclaw', 'crush', 'goose', 'pi',
|
|
41
|
-
'aider', 'amp', 'qwen', 'codex', 'openhands',
|
|
61
|
+
'aider', 'amp', 'qwen', 'codex', 'openhands', 'claude-code',
|
|
42
62
|
]
|
|
43
63
|
|
|
44
64
|
const PROXY_SYNCABLE_CANONICAL = new Set(PROXY_SYNCABLE_TOOLS.map(tool => tool === 'opencode-desktop' ? 'opencode' : tool))
|
|
@@ -343,7 +363,7 @@ function syncEnvTool(proxyInfo, mergedModels, toolMode) {
|
|
|
343
363
|
// 📖 Claude Code: Anthropic-specific env vars
|
|
344
364
|
if (toolMode === 'claude-code') {
|
|
345
365
|
const proxyBase = proxyInfo.baseUrl.replace(/\/v1$/, '')
|
|
346
|
-
envLines.push(`export
|
|
366
|
+
envLines.push(`export ANTHROPIC_API_KEY="${proxyInfo.token}"`)
|
|
347
367
|
envLines.push(`export ANTHROPIC_BASE_URL="${proxyBase}"`)
|
|
348
368
|
}
|
|
349
369
|
|
|
@@ -352,6 +372,12 @@ function syncEnvTool(proxyInfo, mergedModels, toolMode) {
|
|
|
352
372
|
try { copyFileSync(envFilePath, envFilePath + '.bak') } catch { /* best effort */ }
|
|
353
373
|
}
|
|
354
374
|
writeFileSync(envFilePath, envLines.join('\n') + '\n')
|
|
375
|
+
|
|
376
|
+
// 📖 Auto-source the env file in the shell profile so Claude Code picks it up
|
|
377
|
+
if (toolMode === 'claude-code') {
|
|
378
|
+
ensureShellSourceLine(envFilePath)
|
|
379
|
+
}
|
|
380
|
+
|
|
355
381
|
return { path: envFilePath, modelCount: models.length }
|
|
356
382
|
}
|
|
357
383
|
|
package/src/render-table.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - Hotkey-aware header lettering so highlighted letters always match live sort/filter keys
|
|
13
13
|
* - Emoji-aware padding via padEndDisplay for aligned verdict/status cells
|
|
14
14
|
* - Viewport clipping with above/below indicators
|
|
15
|
-
* - Smart badges (mode, tier filter, origin filter
|
|
15
|
+
* - Smart badges (mode, tier filter, origin filter)
|
|
16
16
|
* - Footer J badge: green "Proxy On" / red "Proxy Off" indicator with direct overlay access
|
|
17
17
|
* - Install-endpoints shortcut surfaced directly in the footer hints
|
|
18
18
|
* - Full-width red outdated-version banner when a newer npm release is known
|
|
@@ -93,7 +93,7 @@ export function setActiveProxy(proxyInstance) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
// ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
|
|
96
|
-
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,
|
|
96
|
+
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, proxyStartupStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, proxyEnabled = false, startupLatestVersion = null, versionAlertsEnabled = true, disableWidthsWarning = false) {
|
|
97
97
|
// 📖 Filter out hidden models for display
|
|
98
98
|
const visibleResults = results.filter(r => !r.hidden)
|
|
99
99
|
|
|
@@ -172,11 +172,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
let profileBadge = ''
|
|
177
|
-
if (activeProfile) {
|
|
178
|
-
profileBadge = chalk.bold.rgb(200, 150, 255)(` [📋 ${activeProfile}]`)
|
|
179
|
-
}
|
|
175
|
+
|
|
180
176
|
|
|
181
177
|
// 📖 Column widths (generous spacing with margins)
|
|
182
178
|
const W_RANK = 6
|
|
@@ -230,7 +226,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
230
226
|
const sorted = sortResultsWithPinnedFavorites(visibleResults, sortColumn, sortDirection)
|
|
231
227
|
|
|
232
228
|
const lines = [
|
|
233
|
-
` ${chalk.cyanBright.bold(`🚀 free-coding-models v${LOCAL_VERSION}`)}${modeBadge}${pingControlBadge}${tierBadge}${originBadge}${
|
|
229
|
+
` ${chalk.cyanBright.bold(`🚀 free-coding-models v${LOCAL_VERSION}`)}${modeBadge}${pingControlBadge}${tierBadge}${originBadge}${chalk.reset('')} ` +
|
|
234
230
|
chalk.dim('📦 ') + chalk.cyanBright.bold(`${completedPings}/${totalVisible}`) + chalk.dim(' ') +
|
|
235
231
|
chalk.greenBright(`✅ ${up}`) + chalk.dim(' up ') +
|
|
236
232
|
chalk.yellow(`⏳ ${timeout}`) + chalk.dim(' timeout ') +
|
|
@@ -590,12 +586,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
590
586
|
lines.push(chalk.dim(` ... ${sorted.length - vp.endIdx} more below ...`))
|
|
591
587
|
}
|
|
592
588
|
|
|
593
|
-
|
|
594
|
-
if (profileSaveMode) {
|
|
595
|
-
lines.push(chalk.bgRgb(40, 20, 60)(` 📋 Save profile as: ${chalk.cyanBright(profileSaveBuffer + '▏')} ${chalk.dim('Enter save • Esc cancel')}`))
|
|
596
|
-
} else {
|
|
597
|
-
lines.push('')
|
|
598
|
-
}
|
|
589
|
+
lines.push('')
|
|
599
590
|
// 📖 Footer hints keep only navigation and secondary actions now that the
|
|
600
591
|
// 📖 active tool target is already visible in the header badge.
|
|
601
592
|
const hotkey = (keyLabel, text) => chalk.yellow(keyLabel) + chalk.dim(text)
|
|
@@ -624,11 +615,9 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
624
615
|
chalk.dim(` • `) +
|
|
625
616
|
hotkey('K', ' Help')
|
|
626
617
|
)
|
|
627
|
-
// 📖 Line 2:
|
|
618
|
+
// 📖 Line 2: install flow, recommend, proxy shortcut, feedback, and extended hints.
|
|
628
619
|
lines.push(
|
|
629
620
|
chalk.dim(` `) +
|
|
630
|
-
hotkey('⇧P', ' Cycle profile') + chalk.dim(` • `) +
|
|
631
|
-
hotkey('⇧S', ' Save profile') + chalk.dim(` • `) +
|
|
632
621
|
hotkey('Y', ' Install endpoints') + chalk.dim(` • `) +
|
|
633
622
|
hotkey('Q', ' Smart Recommend') + chalk.dim(` • `) +
|
|
634
623
|
hotkey('I', ' Feedback, bugs & requests')
|
package/src/utils.js
CHANGED
|
@@ -403,17 +403,12 @@ export function parseArgs(argv) {
|
|
|
403
403
|
let apiKey = null
|
|
404
404
|
const flags = []
|
|
405
405
|
|
|
406
|
-
// 📖 Determine which arg indices are consumed by --tier
|
|
406
|
+
// 📖 Determine which arg indices are consumed by --tier so we skip them
|
|
407
407
|
const tierIdx = args.findIndex(a => a.toLowerCase() === '--tier')
|
|
408
408
|
const tierValueIdx = (tierIdx !== -1 && args[tierIdx + 1] && !args[tierIdx + 1].startsWith('--'))
|
|
409
409
|
? tierIdx + 1
|
|
410
410
|
: -1
|
|
411
411
|
|
|
412
|
-
const profileIdx = args.findIndex(a => a.toLowerCase() === '--profile')
|
|
413
|
-
const profileValueIdx = (profileIdx !== -1 && args[profileIdx + 1] && !args[profileIdx + 1].startsWith('--'))
|
|
414
|
-
? profileIdx + 1
|
|
415
|
-
: -1
|
|
416
|
-
|
|
417
412
|
// New value flags
|
|
418
413
|
const sortIdx = args.findIndex(a => a.toLowerCase() === '--sort')
|
|
419
414
|
const sortValueIdx = (sortIdx !== -1 && args[sortIdx + 1] && !args[sortIdx + 1].startsWith('--'))
|
|
@@ -433,7 +428,6 @@ export function parseArgs(argv) {
|
|
|
433
428
|
// 📖 Set of arg indices that are values for flags (not API keys)
|
|
434
429
|
const skipIndices = new Set()
|
|
435
430
|
if (tierValueIdx !== -1) skipIndices.add(tierValueIdx)
|
|
436
|
-
if (profileValueIdx !== -1) skipIndices.add(profileValueIdx)
|
|
437
431
|
if (sortValueIdx !== -1) skipIndices.add(sortValueIdx)
|
|
438
432
|
if (originValueIdx !== -1) skipIndices.add(originValueIdx)
|
|
439
433
|
if (pingIntervalValueIdx !== -1) skipIndices.add(pingIntervalValueIdx)
|
|
@@ -442,7 +436,7 @@ export function parseArgs(argv) {
|
|
|
442
436
|
if (arg.startsWith('--') || arg === '-h') {
|
|
443
437
|
flags.push(arg.toLowerCase())
|
|
444
438
|
} else if (skipIndices.has(i)) {
|
|
445
|
-
// 📖 Skip — this is a value for --tier
|
|
439
|
+
// 📖 Skip — this is a value for --tier, not an API key
|
|
446
440
|
} else if (!apiKey) {
|
|
447
441
|
apiKey = arg
|
|
448
442
|
}
|
|
@@ -465,6 +459,7 @@ export function parseArgs(argv) {
|
|
|
465
459
|
const piMode = flags.includes('--pi')
|
|
466
460
|
const noTelemetry = flags.includes('--no-telemetry')
|
|
467
461
|
const cleanProxyMode = flags.includes('--clean-proxy') || flags.includes('--proxy-clean')
|
|
462
|
+
const proxyForegroundMode = flags.includes('--proxy')
|
|
468
463
|
const jsonMode = flags.includes('--json')
|
|
469
464
|
const helpMode = flags.includes('--help') || flags.includes('-h')
|
|
470
465
|
const premiumMode = flags.includes('--premium')
|
|
@@ -482,7 +477,7 @@ export function parseArgs(argv) {
|
|
|
482
477
|
let pingInterval = pingIntervalValueIdx !== -1 ? parseInt(args[pingIntervalValueIdx], 10) : null
|
|
483
478
|
let sortDirection = sortDesc ? 'desc' : (sortAscFlag ? 'asc' : null)
|
|
484
479
|
|
|
485
|
-
|
|
480
|
+
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
486
481
|
|
|
487
482
|
// 📖 --recommend — launch directly into Smart Recommend mode (Q key equivalent)
|
|
488
483
|
const recommendMode = flags.includes('--recommend')
|
|
@@ -517,8 +512,9 @@ export function parseArgs(argv) {
|
|
|
517
512
|
showUnconfigured,
|
|
518
513
|
disableWidthsWarning,
|
|
519
514
|
premiumMode,
|
|
520
|
-
|
|
521
|
-
recommendMode
|
|
515
|
+
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
516
|
+
recommendMode,
|
|
517
|
+
proxyForegroundMode,
|
|
522
518
|
}
|
|
523
519
|
}
|
|
524
520
|
|