free-coding-models 0.3.9 → 0.3.12
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 +40 -0
- package/README.md +112 -1134
- package/bin/free-coding-models.js +34 -188
- package/package.json +2 -3
- package/src/cli-help.js +0 -18
- package/src/config.js +17 -351
- package/src/endpoint-installer.js +26 -64
- package/src/favorites.js +0 -14
- package/src/key-handler.js +74 -641
- package/src/legacy-proxy-cleanup.js +432 -0
- package/src/openclaw.js +69 -108
- package/src/opencode-config.js +48 -0
- package/src/opencode.js +6 -248
- package/src/overlays.js +26 -550
- package/src/product-flags.js +14 -0
- package/src/render-helpers.js +2 -34
- package/src/render-table.js +14 -33
- package/src/testfcm.js +90 -43
- package/src/token-usage-reader.js +9 -38
- package/src/tool-launchers.js +235 -409
- package/src/tool-metadata.js +0 -7
- package/src/utils.js +8 -77
- package/bin/fcm-proxy-daemon.js +0 -242
- package/src/account-manager.js +0 -634
- package/src/anthropic-translator.js +0 -440
- package/src/daemon-manager.js +0 -527
- package/src/error-classifier.js +0 -154
- package/src/log-reader.js +0 -195
- package/src/opencode-sync.js +0 -200
- package/src/proxy-server.js +0 -1477
- package/src/proxy-sync.js +0 -565
- package/src/proxy-topology.js +0 -85
- package/src/request-transformer.js +0 -180
- package/src/responses-translator.js +0 -423
- package/src/token-stats.js +0 -320
package/src/overlays.js
CHANGED
|
@@ -4,28 +4,24 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @details
|
|
6
6
|
* This module centralizes all overlay rendering in one place:
|
|
7
|
-
* - Settings, Install Endpoints, Help,
|
|
8
|
-
* - FCM Proxy V2 overlay with current-tool auto-sync toggle and cleanup
|
|
7
|
+
* - Settings, Install Endpoints, Help, Smart Recommend, Feedback, Changelog
|
|
9
8
|
* - Settings diagnostics for provider key tests, including wrapped retry/error details
|
|
10
9
|
* - Recommend analysis timer orchestration and progress updates
|
|
11
10
|
*
|
|
12
11
|
* The factory pattern keeps stateful UI logic isolated while still
|
|
13
12
|
* allowing the main CLI to control shared state and dependencies.
|
|
14
13
|
*
|
|
15
|
-
* 📖 The proxy overlay rows are: Enable → Auto-sync current tool → Port → Cleanup → Install/Restart/Stop/Kill/Logs
|
|
16
14
|
* 📖 Feedback overlay (I key) combines feature requests + bug reports in one left-aligned input
|
|
17
15
|
*
|
|
18
16
|
* → Functions:
|
|
19
17
|
* - `createOverlayRenderers` — returns renderer + analysis helpers
|
|
20
18
|
*
|
|
21
19
|
* @exports { createOverlayRenderers }
|
|
22
|
-
* @see ./proxy-sync.js — resolveProxySyncToolMode powers current-tool proxy sync hints
|
|
23
20
|
* @see ./key-handler.js — handles keypresses for all overlay interactions
|
|
24
21
|
*/
|
|
25
22
|
|
|
26
23
|
import { loadChangelog } from './changelog-loader.js'
|
|
27
24
|
import { buildCliHelpLines } from './cli-help.js'
|
|
28
|
-
import { resolveProxySyncToolMode } from './proxy-sync.js'
|
|
29
25
|
|
|
30
26
|
export function createOverlayRenderers(state, deps) {
|
|
31
27
|
const {
|
|
@@ -35,20 +31,16 @@ export function createOverlayRenderers(state, deps) {
|
|
|
35
31
|
PROVIDER_COLOR,
|
|
36
32
|
LOCAL_VERSION,
|
|
37
33
|
getApiKey,
|
|
38
|
-
getProxySettings,
|
|
39
34
|
resolveApiKeys,
|
|
40
35
|
isProviderEnabled,
|
|
41
|
-
listProfiles,
|
|
42
36
|
TIER_CYCLE,
|
|
43
37
|
SETTINGS_OVERLAY_BG,
|
|
44
38
|
HELP_OVERLAY_BG,
|
|
45
39
|
RECOMMEND_OVERLAY_BG,
|
|
46
|
-
LOG_OVERLAY_BG,
|
|
47
40
|
OVERLAY_PANEL_WIDTH,
|
|
48
41
|
keepOverlayTargetVisible,
|
|
49
42
|
sliceOverlayLines,
|
|
50
43
|
tintOverlayLines,
|
|
51
|
-
loadRecentLogs,
|
|
52
44
|
TASK_TYPES,
|
|
53
45
|
PRIORITY_TYPES,
|
|
54
46
|
CONTEXT_BUDGETS,
|
|
@@ -63,7 +55,6 @@ export function createOverlayRenderers(state, deps) {
|
|
|
63
55
|
getConfiguredInstallableProviders,
|
|
64
56
|
getInstallTargetModes,
|
|
65
57
|
getProviderCatalogModels,
|
|
66
|
-
CONNECTION_MODES,
|
|
67
58
|
getToolMeta,
|
|
68
59
|
} = deps
|
|
69
60
|
|
|
@@ -90,54 +81,6 @@ export function createOverlayRenderers(state, deps) {
|
|
|
90
81
|
return lines
|
|
91
82
|
}
|
|
92
83
|
|
|
93
|
-
// 📖 Keep log token formatting aligned with the main table so the same totals
|
|
94
|
-
// 📖 read the same everywhere in the TUI.
|
|
95
|
-
const formatLogTokens = (totalTokens) => {
|
|
96
|
-
const safeTotal = Number(totalTokens) || 0
|
|
97
|
-
if (safeTotal <= 0) return '--'
|
|
98
|
-
if (safeTotal >= 999_500) return `${(safeTotal / 1_000_000).toFixed(2)}M`
|
|
99
|
-
if (safeTotal >= 1_000) return `${(safeTotal / 1_000).toFixed(2)}k`
|
|
100
|
-
return String(Math.floor(safeTotal))
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 📖 Colorize latency with gradient: green (<500ms) → orange (<1000ms) → yellow (<1500ms) → red (>=1500ms)
|
|
104
|
-
const colorizeLatency = (latency, text) => {
|
|
105
|
-
const ms = Number(latency) || 0
|
|
106
|
-
if (ms <= 0) return chalk.dim(text)
|
|
107
|
-
if (ms < 500) return chalk.greenBright(text)
|
|
108
|
-
if (ms < 1000) return chalk.rgb(255, 165, 0)(text) // Orange
|
|
109
|
-
if (ms < 1500) return chalk.yellow(text)
|
|
110
|
-
return chalk.red(text)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 📖 Colorize tokens with gradient: dim green (few) → bright green (many)
|
|
114
|
-
const colorizeTokens = (tokens, text) => {
|
|
115
|
-
const tok = Number(tokens) || 0
|
|
116
|
-
if (tok <= 0) return chalk.dim(text)
|
|
117
|
-
// Gradient: light green (low) → medium green → bright green (high, >30k)
|
|
118
|
-
if (tok < 10_000) return chalk.hex('#90EE90')(text) // Light green
|
|
119
|
-
if (tok < 30_000) return chalk.hex('#32CD32')(text) // Lime green
|
|
120
|
-
return chalk.greenBright(text) // Full brightness green
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 📖 Get model color based on status code - distinct colors for each error type
|
|
124
|
-
const getModelColorByStatus = (status) => {
|
|
125
|
-
const sc = String(status)
|
|
126
|
-
if (sc === '200') return chalk.greenBright // Success - bright green
|
|
127
|
-
if (sc === '404') return chalk.rgb(139, 0, 0) // Not found - dark red
|
|
128
|
-
if (sc === '400') return chalk.hex('#8B008B') // Bad request - dark magenta
|
|
129
|
-
if (sc === '401') return chalk.hex('#9932CC') // Unauthorized - dark orchid
|
|
130
|
-
if (sc === '403') return chalk.hex('#BA55D3') // Forbidden - medium orchid
|
|
131
|
-
if (sc === '413') return chalk.hex('#FF6347') // Payload too large - tomato red
|
|
132
|
-
if (sc === '429') return chalk.hex('#FFB90F') // Rate limit - dark orange
|
|
133
|
-
if (sc === '500') return chalk.hex('#DC143C') // Internal server error - crimson
|
|
134
|
-
if (sc === '502') return chalk.hex('#C71585') // Bad gateway - medium violet red
|
|
135
|
-
if (sc === '503') return chalk.hex('#9370DB') // Service unavailable - medium purple
|
|
136
|
-
if (sc.startsWith('5')) return chalk.magenta // Other 5xx - magenta
|
|
137
|
-
if (sc === '0') return chalk.hex('#696969') // Timeout/error - dim gray
|
|
138
|
-
return chalk.white // Unknown - white
|
|
139
|
-
}
|
|
140
|
-
|
|
141
84
|
// ─── Settings screen renderer ─────────────────────────────────────────────
|
|
142
85
|
// 📖 renderSettings: Draw the settings overlay in the alt screen buffer.
|
|
143
86
|
// 📖 Shows all providers with their API key (masked) + enabled state.
|
|
@@ -145,11 +88,10 @@ export function createOverlayRenderers(state, deps) {
|
|
|
145
88
|
// 📖 Key "T" in settings = test API key for selected provider.
|
|
146
89
|
function renderSettings() {
|
|
147
90
|
const providerKeys = Object.keys(sources)
|
|
148
|
-
const updateRowIdx = providerKeys.length
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const proxySettings = getProxySettings(state.config)
|
|
91
|
+
const updateRowIdx = providerKeys.length
|
|
92
|
+
const widthWarningRowIdx = updateRowIdx + 1
|
|
93
|
+
const cleanupLegacyProxyRowIdx = widthWarningRowIdx + 1
|
|
94
|
+
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
153
95
|
const EL = '\x1b[K'
|
|
154
96
|
const lines = []
|
|
155
97
|
const cursorLineByRow = {}
|
|
@@ -286,23 +228,11 @@ const updateRowIdx = providerKeys.length
|
|
|
286
228
|
lines.push(chalk.red(` ${state.settingsUpdateError}`))
|
|
287
229
|
}
|
|
288
230
|
|
|
289
|
-
// 📖
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
lines.push(
|
|
294
|
-
|
|
295
|
-
const proxyDaemonBullet = state.settingsCursor === proxyDaemonRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
296
|
-
const proxyStatus = proxySettings.enabled ? chalk.greenBright('Proxy ON') : chalk.dim('Proxy OFF')
|
|
297
|
-
const daemonStatus = state.daemonStatus || 'not-installed'
|
|
298
|
-
let daemonBadge
|
|
299
|
-
if (daemonStatus === 'running') daemonBadge = chalk.greenBright('Service ON')
|
|
300
|
-
else if (daemonStatus === 'stopped') daemonBadge = chalk.yellow('Service stopped')
|
|
301
|
-
else if (daemonStatus === 'stale' || daemonStatus === 'unhealthy') daemonBadge = chalk.red('Service ' + daemonStatus)
|
|
302
|
-
else daemonBadge = chalk.dim('Service OFF')
|
|
303
|
-
const proxyDaemonRow = `${proxyDaemonBullet}${chalk.bold('FCM Proxy V2 settings →').padEnd(44)} ${proxyStatus} ${chalk.dim('•')} ${daemonBadge}`
|
|
304
|
-
cursorLineByRow[proxyDaemonRowIdx] = lines.length
|
|
305
|
-
lines.push(state.settingsCursor === proxyDaemonRowIdx ? chalk.bgRgb(20, 45, 60)(proxyDaemonRow) : proxyDaemonRow)
|
|
231
|
+
// 📖 Cleanup row removes stale proxy-era config left behind by older builds.
|
|
232
|
+
const cleanupLegacyProxyBullet = state.settingsCursor === cleanupLegacyProxyRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
233
|
+
const cleanupLegacyProxyRow = `${cleanupLegacyProxyBullet}${chalk.bold('Clean Legacy Proxy Config').padEnd(44)} ${chalk.magentaBright('Enter remove discontinued bridge leftovers')}`
|
|
234
|
+
cursorLineByRow[cleanupLegacyProxyRowIdx] = lines.length
|
|
235
|
+
lines.push(state.settingsCursor === cleanupLegacyProxyRowIdx ? chalk.bgRgb(55, 25, 55)(cleanupLegacyProxyRow) : cleanupLegacyProxyRow)
|
|
306
236
|
|
|
307
237
|
// 📖 Changelog viewer row
|
|
308
238
|
const changelogViewBullet = state.settingsCursor === changelogViewRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
@@ -310,41 +240,13 @@ const updateRowIdx = providerKeys.length
|
|
|
310
240
|
cursorLineByRow[changelogViewRowIdx] = lines.length
|
|
311
241
|
lines.push(state.settingsCursor === changelogViewRowIdx ? chalk.bgRgb(30, 45, 30)(changelogViewRow) : changelogViewRow)
|
|
312
242
|
|
|
313
|
-
// 📖
|
|
314
|
-
const savedProfiles = listProfiles(state.config)
|
|
315
|
-
const profileStartIdx = updateRowIdx + 5
|
|
316
|
-
const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : changelogViewRowIdx
|
|
317
|
-
|
|
318
|
-
lines.push('')
|
|
319
|
-
lines.push(` ${chalk.bold('📋 Profiles')} ${chalk.dim(savedProfiles.length > 0 ? `(${savedProfiles.length} saved)` : '(none — press Shift+S in main view to save)')}`)
|
|
320
|
-
lines.push(` ${chalk.dim(' ' + '─'.repeat(separatorWidth))}`)
|
|
321
|
-
lines.push('')
|
|
322
|
-
|
|
323
|
-
if (savedProfiles.length === 0) {
|
|
324
|
-
lines.push(chalk.dim(' No saved profiles. Press Shift+S in the main table to save your current settings as a profile.'))
|
|
325
|
-
} else {
|
|
326
|
-
for (let i = 0; i < savedProfiles.length; i++) {
|
|
327
|
-
const pName = savedProfiles[i]
|
|
328
|
-
const rowIdx = profileStartIdx + i
|
|
329
|
-
const isCursor = state.settingsCursor === rowIdx
|
|
330
|
-
const isActive = state.activeProfile === pName
|
|
331
|
-
const activeBadge = isActive ? chalk.greenBright(' ✅ active') : ''
|
|
332
|
-
const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
333
|
-
const profileLabel = chalk.rgb(200, 150, 255).bold(pName.padEnd(30))
|
|
334
|
-
const deleteHint = isCursor ? chalk.dim(' Enter→Load • Backspace→Delete') : ''
|
|
335
|
-
const row = `${bullet}${profileLabel}${activeBadge}${deleteHint}`
|
|
336
|
-
cursorLineByRow[rowIdx] = lines.length
|
|
337
|
-
lines.push(isCursor ? chalk.bgRgb(40, 20, 60)(row) : row)
|
|
338
|
-
}
|
|
339
|
-
}
|
|
243
|
+
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
340
244
|
|
|
341
245
|
lines.push('')
|
|
342
246
|
if (state.settingsEditMode) {
|
|
343
247
|
lines.push(chalk.dim(' Type API key • Enter Save • Esc Cancel'))
|
|
344
|
-
} else if (state.settingsProxyPortEditMode) {
|
|
345
|
-
lines.push(chalk.dim(' Type proxy port (0 = auto) • Enter Save • Esc Cancel'))
|
|
346
248
|
} else {
|
|
347
|
-
lines.push(chalk.dim(' ↑↓ Navigate • Enter Edit/Run • + Add key • - Remove key • Space Toggle • T Test key •
|
|
249
|
+
lines.push(chalk.dim(' ↑↓ Navigate • Enter Edit/Run • + Add key • - Remove key • Space Toggle • T Test key • U Updates • Esc Close'))
|
|
348
250
|
}
|
|
349
251
|
// 📖 Show sync/restore status message if set
|
|
350
252
|
if (state.settingsSyncStatus) {
|
|
@@ -382,7 +284,7 @@ const profileStartIdx = updateRowIdx + 5
|
|
|
382
284
|
}
|
|
383
285
|
|
|
384
286
|
// ─── Install Endpoints overlay renderer ───────────────────────────────────
|
|
385
|
-
// 📖 renderInstallEndpoints drives the provider → tool →
|
|
287
|
+
// 📖 renderInstallEndpoints drives the provider → tool → scope → model flow
|
|
386
288
|
// 📖 behind the `Y` hotkey. It deliberately reuses the same overlay viewport
|
|
387
289
|
// 📖 helpers as Settings so long provider/model lists stay navigable.
|
|
388
290
|
function renderInstallEndpoints() {
|
|
@@ -391,8 +293,7 @@ const profileStartIdx = updateRowIdx + 5
|
|
|
391
293
|
const cursorLineByRow = {}
|
|
392
294
|
const providerChoices = getConfiguredInstallableProviders(state.config)
|
|
393
295
|
const toolChoices = getInstallTargetModes()
|
|
394
|
-
const
|
|
395
|
-
const totalSteps = 5
|
|
296
|
+
const totalSteps = 4
|
|
396
297
|
const scopeChoices = [
|
|
397
298
|
{
|
|
398
299
|
key: 'all',
|
|
@@ -418,11 +319,7 @@ const profileStartIdx = updateRowIdx + 5
|
|
|
418
319
|
})()
|
|
419
320
|
: '—'
|
|
420
321
|
|
|
421
|
-
const selectedConnectionLabel =
|
|
422
|
-
? 'FCM Proxy V2'
|
|
423
|
-
: state.installEndpointsConnectionMode === 'direct'
|
|
424
|
-
? 'Direct Provider'
|
|
425
|
-
: '—'
|
|
322
|
+
const selectedConnectionLabel = 'Direct Provider'
|
|
426
323
|
|
|
427
324
|
lines.push('')
|
|
428
325
|
// 📖 Branding header
|
|
@@ -466,7 +363,7 @@ const profileStartIdx = updateRowIdx + 5
|
|
|
466
363
|
const label = `${meta.emoji} ${meta.label}`
|
|
467
364
|
const note = toolMode.startsWith('opencode')
|
|
468
365
|
? chalk.dim('shared config file')
|
|
469
|
-
:
|
|
366
|
+
: toolMode === 'openhands'
|
|
470
367
|
? chalk.dim('env file (~/.fcm-*-env)')
|
|
471
368
|
: chalk.dim('managed config install')
|
|
472
369
|
const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
@@ -477,26 +374,8 @@ const profileStartIdx = updateRowIdx + 5
|
|
|
477
374
|
|
|
478
375
|
lines.push('')
|
|
479
376
|
lines.push(chalk.dim(' ↑↓ Navigate • Enter Choose tool • Esc Back'))
|
|
480
|
-
} else if (state.installEndpointsPhase === 'connection') {
|
|
481
|
-
// 📖 Step 3: Choose connection mode — Direct Provider vs FCM Proxy
|
|
482
|
-
lines.push(` ${chalk.bold(`Step 3/${totalSteps}`)} ${chalk.cyan('Choose connection mode')}`)
|
|
483
|
-
lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel}`))
|
|
484
|
-
lines.push('')
|
|
485
|
-
|
|
486
|
-
connectionChoices.forEach((mode, idx) => {
|
|
487
|
-
const isCursor = idx === state.installEndpointsCursor
|
|
488
|
-
const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
489
|
-
const icon = mode.key === 'proxy' ? '🔄' : '⚡'
|
|
490
|
-
const row = `${bullet}${icon} ${chalk.bold(mode.label)}`
|
|
491
|
-
cursorLineByRow[idx] = lines.length
|
|
492
|
-
lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
|
|
493
|
-
lines.push(chalk.dim(` ${mode.hint}`))
|
|
494
|
-
lines.push('')
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
lines.push(chalk.dim(' Enter Continue • Esc Back'))
|
|
498
377
|
} else if (state.installEndpointsPhase === 'scope') {
|
|
499
|
-
lines.push(` ${chalk.bold(`Step
|
|
378
|
+
lines.push(` ${chalk.bold(`Step 3/${totalSteps}`)} ${chalk.cyan('Choose the install scope')}`)
|
|
500
379
|
lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel} • ${selectedConnectionLabel}`))
|
|
501
380
|
lines.push('')
|
|
502
381
|
|
|
@@ -515,7 +394,7 @@ const profileStartIdx = updateRowIdx + 5
|
|
|
515
394
|
const models = getProviderCatalogModels(state.installEndpointsProviderKey)
|
|
516
395
|
const selectedCount = state.installEndpointsSelectedModelIds.size
|
|
517
396
|
|
|
518
|
-
lines.push(` ${chalk.bold(`Step
|
|
397
|
+
lines.push(` ${chalk.bold(`Step 4/${totalSteps}`)} ${chalk.cyan('Choose which models to install')}`)
|
|
519
398
|
lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel} • ${selectedConnectionLabel}`))
|
|
520
399
|
lines.push(chalk.dim(` Selected: ${selectedCount}/${models.length}`))
|
|
521
400
|
lines.push('')
|
|
@@ -617,8 +496,8 @@ const profileStartIdx = updateRowIdx + 5
|
|
|
617
496
|
lines.push(` ${chalk.cyan('Up%')} Uptime — ratio of successful pings to total pings ${chalk.dim('Sort:')} ${chalk.yellow('U')}`)
|
|
618
497
|
lines.push(` ${chalk.dim('If a model only works half the time, you\'ll waste time retrying. Higher = more reliable.')}`)
|
|
619
498
|
lines.push('')
|
|
620
|
-
lines.push(` ${chalk.cyan('Used')}
|
|
621
|
-
lines.push(` ${chalk.dim('Loaded
|
|
499
|
+
lines.push(` ${chalk.cyan('Used')} Historical prompt+completion tokens tracked for this exact provider/model pair`)
|
|
500
|
+
lines.push(` ${chalk.dim('Loaded from local stats snapshots. Displayed in K tokens, or M tokens above one million.')}`)
|
|
622
501
|
lines.push('')
|
|
623
502
|
|
|
624
503
|
|
|
@@ -630,19 +509,14 @@ const profileStartIdx = updateRowIdx + 5
|
|
|
630
509
|
lines.push('')
|
|
631
510
|
lines.push(` ${chalk.bold('Controls')}`)
|
|
632
511
|
lines.push(` ${chalk.yellow('W')} Toggle ping mode ${chalk.dim('(speed 2s → normal 10s → slow 30s → forced 4s)')}`)
|
|
633
|
-
lines.push(` ${chalk.yellow('E')} Toggle configured models only ${chalk.dim('(enabled by default
|
|
634
|
-
lines.push(` ${chalk.yellow('
|
|
635
|
-
lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Claude Code → Codex → Gemini → Qwen → OpenHands → Amp)')}`)
|
|
512
|
+
lines.push(` ${chalk.yellow('E')} Toggle configured models only ${chalk.dim('(enabled by default)')}`)
|
|
513
|
+
lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Qwen → OpenHands → Amp)')}`)
|
|
636
514
|
lines.push(` ${chalk.yellow('F')} Toggle favorite on selected row ${chalk.dim('(⭐ pinned at top, persisted)')}`)
|
|
637
|
-
lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog → compatible tools,
|
|
515
|
+
lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog → compatible tools, direct provider only)')}`)
|
|
638
516
|
lines.push(` ${chalk.yellow('Q')} Smart Recommend ${chalk.dim('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
|
|
639
517
|
lines.push(` ${chalk.rgb(255, 87, 51).bold('I')} Feedback, bugs & requests ${chalk.dim('(📝 send anonymous feedback, bug reports, or feature requests)')}`)
|
|
640
|
-
lines.push(` ${chalk.yellow('
|
|
641
|
-
|
|
642
|
-
lines.push(` ${chalk.yellow('Shift+P')} Cycle config profile ${chalk.dim('(switch between saved profiles live)')}`)
|
|
643
|
-
lines.push(` ${chalk.yellow('Shift+S')} Save current config as a named profile ${chalk.dim('(inline prompt — type name + Enter)')}`)
|
|
644
|
-
lines.push(` ${chalk.dim('Profiles store: favorites, sort, tier filter, ping interval, configured-only filter, API keys.')}`)
|
|
645
|
-
lines.push(` ${chalk.dim('Use --profile <name> to load a profile on startup.')}`)
|
|
518
|
+
lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, updates, legacy cleanup)')}`)
|
|
519
|
+
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
646
520
|
lines.push(` ${chalk.yellow('Shift+R')} Reset view settings ${chalk.dim('(tier filter, sort, provider filter → defaults)')}`)
|
|
647
521
|
lines.push(` ${chalk.yellow('N')} Changelog ${chalk.dim('(📋 browse all versions, Enter to view details)')}`)
|
|
648
522
|
lines.push(` ${chalk.yellow('K')} / ${chalk.yellow('Esc')} Show/hide this help`)
|
|
@@ -652,7 +526,7 @@ const profileStartIdx = updateRowIdx + 5
|
|
|
652
526
|
lines.push(` ${chalk.yellow('↑↓')} Navigate rows`)
|
|
653
527
|
lines.push(` ${chalk.yellow('PgUp/PgDn')} Jump by page`)
|
|
654
528
|
lines.push(` ${chalk.yellow('Home/End')} Jump first/last row`)
|
|
655
|
-
lines.push(` ${chalk.yellow('Enter')} Edit key /
|
|
529
|
+
lines.push(` ${chalk.yellow('Enter')} Edit key / run selected maintenance action`)
|
|
656
530
|
lines.push(` ${chalk.yellow('Space')} Toggle provider enable/disable`)
|
|
657
531
|
lines.push(` ${chalk.yellow('T')} Test selected provider key`)
|
|
658
532
|
lines.push(` ${chalk.yellow('U')} Check updates manually`)
|
|
@@ -668,173 +542,6 @@ const profileStartIdx = updateRowIdx + 5
|
|
|
668
542
|
return cleared.join('\n')
|
|
669
543
|
}
|
|
670
544
|
|
|
671
|
-
// ─── Log page overlay renderer ────────────────────────────────────────────
|
|
672
|
-
// 📖 renderLog: Draw the log page overlay showing recent requests from
|
|
673
|
-
// 📖 ~/.free-coding-models/request-log.jsonl, newest-first.
|
|
674
|
-
// 📖 Toggled with X key. Esc or X closes.
|
|
675
|
-
function renderLog() {
|
|
676
|
-
const EL = '\x1b[K'
|
|
677
|
-
const lines = []
|
|
678
|
-
|
|
679
|
-
// 📖 Branding header
|
|
680
|
-
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
|
|
681
|
-
lines.push(` ${chalk.bold('📋 Request Log')}`)
|
|
682
|
-
lines.push('')
|
|
683
|
-
lines.push(chalk.dim(' — recent requests • ↑↓ scroll • A toggle all/500 • X or Esc close'))
|
|
684
|
-
lines.push(chalk.dim(' Works only when the multi-account proxy is enabled and requests go through it.'))
|
|
685
|
-
lines.push(chalk.dim(' Direct provider launches do not currently write into this log.'))
|
|
686
|
-
|
|
687
|
-
// 📖 Load recent log entries — bounded read, newest-first, malformed lines skipped.
|
|
688
|
-
// 📖 Show up to 500 entries by default, or all if logShowAll is true.
|
|
689
|
-
const logLimit = state.logShowAll ? Number.MAX_SAFE_INTEGER : 500
|
|
690
|
-
const logRows = loadRecentLogs({ limit: logLimit })
|
|
691
|
-
const totalTokens = logRows.reduce((sum, row) => sum + (Number(row.tokens) || 0), 0)
|
|
692
|
-
|
|
693
|
-
if (logRows.length === 0) {
|
|
694
|
-
lines.push(chalk.dim(' No log entries found.'))
|
|
695
|
-
lines.push(chalk.dim(' Logs are written to ~/.free-coding-models/request-log.jsonl'))
|
|
696
|
-
lines.push(chalk.dim(' when requests are proxied through the multi-account rotation proxy.'))
|
|
697
|
-
lines.push(chalk.dim(' Direct provider launches do not currently feed this token log.'))
|
|
698
|
-
} else {
|
|
699
|
-
lines.push(` ${chalk.bold('Total Consumed:')} ${chalk.greenBright(formatLogTokens(totalTokens))}`)
|
|
700
|
-
lines.push('')
|
|
701
|
-
// 📖 Column widths for the log table
|
|
702
|
-
const W_TIME = 19
|
|
703
|
-
const W_PROV = 14
|
|
704
|
-
const W_MODEL = 44
|
|
705
|
-
const W_ROUTE = 18
|
|
706
|
-
const W_STATUS = 8
|
|
707
|
-
const W_TOKENS = 12
|
|
708
|
-
const W_LAT = 10
|
|
709
|
-
|
|
710
|
-
// 📖 Header row
|
|
711
|
-
const hTime = chalk.dim('Time'.padEnd(W_TIME))
|
|
712
|
-
const hProv = chalk.dim('Provider'.padEnd(W_PROV))
|
|
713
|
-
const hModel = chalk.dim('Model'.padEnd(W_MODEL))
|
|
714
|
-
const hRoute = chalk.dim('Route'.padEnd(W_ROUTE))
|
|
715
|
-
const hStatus = chalk.dim('Status'.padEnd(W_STATUS))
|
|
716
|
-
const hTok = chalk.dim('Tokens Used'.padEnd(W_TOKENS))
|
|
717
|
-
const hLat = chalk.dim('Latency'.padEnd(W_LAT))
|
|
718
|
-
|
|
719
|
-
// 📖 Show mode indicator (all vs limited)
|
|
720
|
-
const modeBadge = state.logShowAll
|
|
721
|
-
? chalk.yellow.bold('ALL')
|
|
722
|
-
: chalk.cyan.bold('500')
|
|
723
|
-
const countBadge = chalk.dim(`Showing ${logRows.length} entries`)
|
|
724
|
-
|
|
725
|
-
lines.push(` ${hTime} ${hProv} ${hModel} ${hRoute} ${hStatus} ${hTok} ${hLat}`)
|
|
726
|
-
lines.push(` ${chalk.dim('─'.repeat(W_TIME + W_PROV + W_MODEL + W_ROUTE + W_STATUS + W_TOKENS + W_LAT + 12))} ${modeBadge} ${countBadge}`)
|
|
727
|
-
|
|
728
|
-
for (const row of logRows) {
|
|
729
|
-
// 📖 Format time as HH:MM:SS (strip the date part for compactness)
|
|
730
|
-
let timeStr = row.time
|
|
731
|
-
try {
|
|
732
|
-
const d = new Date(row.time)
|
|
733
|
-
if (!Number.isNaN(d.getTime())) {
|
|
734
|
-
timeStr = d.toISOString().replace('T', ' ').slice(0, 19)
|
|
735
|
-
}
|
|
736
|
-
} catch { /* keep raw */ }
|
|
737
|
-
|
|
738
|
-
const requestedModelLabel = row.requestedModel || ''
|
|
739
|
-
// 📖 Always show "requested → actual" if they differ, not just when switched
|
|
740
|
-
const displayModel = requestedModelLabel && requestedModelLabel !== row.model
|
|
741
|
-
? `${requestedModelLabel} → ${row.model}`
|
|
742
|
-
: row.model
|
|
743
|
-
|
|
744
|
-
// 📖 Color-code status with distinct colors for each error type
|
|
745
|
-
let statusCell
|
|
746
|
-
const sc = String(row.status)
|
|
747
|
-
if (sc === '200') {
|
|
748
|
-
statusCell = chalk.greenBright(sc.padEnd(W_STATUS))
|
|
749
|
-
} else if (sc === '404') {
|
|
750
|
-
statusCell = chalk.rgb(139, 0, 0).bold(sc.padEnd(W_STATUS)) // Dark red for 404
|
|
751
|
-
} else if (sc === '400') {
|
|
752
|
-
statusCell = chalk.hex('#8B008B').bold(sc.padEnd(W_STATUS)) // Dark magenta
|
|
753
|
-
} else if (sc === '401') {
|
|
754
|
-
statusCell = chalk.hex('#9932CC').bold(sc.padEnd(W_STATUS)) // Dark orchid
|
|
755
|
-
} else if (sc === '403') {
|
|
756
|
-
statusCell = chalk.hex('#BA55D3').bold(sc.padEnd(W_STATUS)) // Medium orchid
|
|
757
|
-
} else if (sc === '413') {
|
|
758
|
-
statusCell = chalk.hex('#FF6347').bold(sc.padEnd(W_STATUS)) // Tomato red
|
|
759
|
-
} else if (sc === '429') {
|
|
760
|
-
statusCell = chalk.hex('#FFB90F').bold(sc.padEnd(W_STATUS)) // Dark orange
|
|
761
|
-
} else if (sc === '500') {
|
|
762
|
-
statusCell = chalk.hex('#DC143C').bold(sc.padEnd(W_STATUS)) // Crimson
|
|
763
|
-
} else if (sc === '502') {
|
|
764
|
-
statusCell = chalk.hex('#C71585').bold(sc.padEnd(W_STATUS)) // Medium violet red
|
|
765
|
-
} else if (sc === '503') {
|
|
766
|
-
statusCell = chalk.hex('#9370DB').bold(sc.padEnd(W_STATUS)) // Medium purple
|
|
767
|
-
} else if (sc.startsWith('5')) {
|
|
768
|
-
statusCell = chalk.magenta(sc.padEnd(W_STATUS)) // Other 5xx - magenta
|
|
769
|
-
} else if (sc === '0') {
|
|
770
|
-
statusCell = chalk.hex('#696969')(sc.padEnd(W_STATUS)) // Dim gray for timeout
|
|
771
|
-
} else {
|
|
772
|
-
statusCell = chalk.dim(sc.padEnd(W_STATUS))
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const tokStr = formatLogTokens(row.tokens)
|
|
776
|
-
const latStr = row.latency > 0 ? `${row.latency}ms` : '--'
|
|
777
|
-
const routeLabel = row.switched
|
|
778
|
-
? `SWITCHED ↻ ${row.switchReason || 'fallback'}`
|
|
779
|
-
: 'direct'
|
|
780
|
-
|
|
781
|
-
// 📖 Detect failed requests with zero tokens - these get special red highlighting
|
|
782
|
-
const isFailedWithZeroTokens = row.status !== '200' && (!row.tokens || Number(row.tokens) === 0)
|
|
783
|
-
|
|
784
|
-
const timeCell = chalk.dim(timeStr.slice(0, W_TIME).padEnd(W_TIME))
|
|
785
|
-
// 📖 Provider display: Use pretty label if available, otherwise raw key.
|
|
786
|
-
// 📖 All these logs are from FCM Proxy V2.
|
|
787
|
-
const providerLabel = PROVIDER_METADATA[row.provider]?.label || row.provider
|
|
788
|
-
const providerRgb = PROVIDER_COLOR[row.provider] ?? [105, 190, 245]
|
|
789
|
-
const provCell = chalk.bold.rgb(...providerRgb)(providerLabel.slice(0, W_PROV).padEnd(W_PROV))
|
|
790
|
-
|
|
791
|
-
// 📖 Color model based on status - red for failed requests with zero tokens
|
|
792
|
-
let modelCell
|
|
793
|
-
if (isFailedWithZeroTokens) {
|
|
794
|
-
modelCell = chalk.red.bold(displayModel.slice(0, W_MODEL).padEnd(W_MODEL))
|
|
795
|
-
} else {
|
|
796
|
-
const modelColorFn = getModelColorByStatus(row.status)
|
|
797
|
-
modelCell = row.switched
|
|
798
|
-
? chalk.bold.rgb(255, 210, 90)(displayModel.slice(0, W_MODEL).padEnd(W_MODEL))
|
|
799
|
-
: modelColorFn(displayModel.slice(0, W_MODEL).padEnd(W_MODEL))
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
const routeCell = row.switched
|
|
803
|
-
? chalk.bgRgb(120, 25, 25).yellow.bold(` ${routeLabel.slice(0, W_ROUTE - 2).padEnd(W_ROUTE - 2)} `)
|
|
804
|
-
: chalk.dim(routeLabel.padEnd(W_ROUTE))
|
|
805
|
-
|
|
806
|
-
// 📖 Colorize tokens - red cross emoji for failed requests with zero tokens
|
|
807
|
-
let tokCell
|
|
808
|
-
if (isFailedWithZeroTokens) {
|
|
809
|
-
tokCell = chalk.red.bold('✗'.padEnd(W_TOKENS))
|
|
810
|
-
} else {
|
|
811
|
-
tokCell = colorizeTokens(row.tokens, tokStr.padEnd(W_TOKENS))
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// 📖 Colorize latency with gradient (green → orange → yellow → red)
|
|
815
|
-
const latCell = colorizeLatency(row.latency, latStr.padEnd(W_LAT))
|
|
816
|
-
|
|
817
|
-
// 📖 Build the row line - add dark red background for failed requests with zero tokens
|
|
818
|
-
const rowText = ` ${timeCell} ${provCell} ${modelCell} ${routeCell} ${statusCell} ${tokCell} ${latCell}`
|
|
819
|
-
if (isFailedWithZeroTokens) {
|
|
820
|
-
lines.push(chalk.bgRgb(40, 0, 0)(rowText))
|
|
821
|
-
} else {
|
|
822
|
-
lines.push(rowText)
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
lines.push('')
|
|
828
|
-
lines.push(chalk.dim(` Showing up to 200 most recent entries • X or Esc close`))
|
|
829
|
-
lines.push('')
|
|
830
|
-
|
|
831
|
-
const { visible, offset } = sliceOverlayLines(lines, state.logScrollOffset, state.terminalRows)
|
|
832
|
-
state.logScrollOffset = offset
|
|
833
|
-
const tintedLines = tintOverlayLines(visible, LOG_OVERLAY_BG, state.terminalCols)
|
|
834
|
-
const cleared = tintedLines.map(l => l + EL)
|
|
835
|
-
return cleared.join('\n')
|
|
836
|
-
}
|
|
837
|
-
|
|
838
545
|
// 📖 renderRecommend: Draw the Smart Recommend overlay with 3 phases:
|
|
839
546
|
// 1. 'questionnaire' — ask 3 questions (task type, priority, context budget)
|
|
840
547
|
// 2. 'analyzing' — loading screen with progress bar (10s, 2 pings/sec)
|
|
@@ -1249,235 +956,6 @@ const profileStartIdx = updateRowIdx + 5
|
|
|
1249
956
|
return cleared.join('\n')
|
|
1250
957
|
}
|
|
1251
958
|
|
|
1252
|
-
// ─── FCM Proxy V2 overlay renderer ──────────────────────────────────────────
|
|
1253
|
-
// 📖 renderProxyDaemon: Dedicated full-page overlay for FCM Proxy V2 configuration
|
|
1254
|
-
// 📖 and background service management. Opened from Settings → "FCM Proxy V2 settings →".
|
|
1255
|
-
// 📖 Contains all proxy toggles, service status/actions, explanations, and emergency kill.
|
|
1256
|
-
function renderProxyDaemon() {
|
|
1257
|
-
const EL = '\x1b[K'
|
|
1258
|
-
const lines = []
|
|
1259
|
-
const cursorLineByRow = {}
|
|
1260
|
-
const proxySettings = getProxySettings(state.config)
|
|
1261
|
-
|
|
1262
|
-
// 📖 Row indices — these control cursor navigation
|
|
1263
|
-
const ROW_PROXY_ENABLED = 0
|
|
1264
|
-
const ROW_PROXY_SYNC = 1
|
|
1265
|
-
const ROW_PROXY_PORT = 2
|
|
1266
|
-
const ROW_PROXY_CLEANUP = 3
|
|
1267
|
-
const ROW_DAEMON_INSTALL = 4
|
|
1268
|
-
const ROW_DAEMON_RESTART = 5
|
|
1269
|
-
const ROW_DAEMON_STOP = 6
|
|
1270
|
-
const ROW_DAEMON_KILL = 7
|
|
1271
|
-
const ROW_DAEMON_LOGS = 8
|
|
1272
|
-
|
|
1273
|
-
const daemonStatus = state.daemonStatus || 'not-installed'
|
|
1274
|
-
const daemonInfo = state.daemonInfo
|
|
1275
|
-
const daemonIsActive = daemonStatus === 'running' || daemonStatus === 'unhealthy' || daemonStatus === 'stale'
|
|
1276
|
-
const daemonIsInstalled = daemonIsActive || daemonStatus === 'stopped'
|
|
1277
|
-
|
|
1278
|
-
// 📖 Compute max row — hide daemon action rows when daemon not installed
|
|
1279
|
-
let maxRow = ROW_DAEMON_INSTALL
|
|
1280
|
-
if (daemonIsInstalled) maxRow = ROW_DAEMON_LOGS
|
|
1281
|
-
|
|
1282
|
-
// 📖 Header
|
|
1283
|
-
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
|
|
1284
|
-
lines.push(` ${chalk.bold('📡 FCM Proxy V2 Manager')}`)
|
|
1285
|
-
lines.push(` ${chalk.dim('— Esc back to Settings • ↑↓ navigate • Enter select')}`)
|
|
1286
|
-
lines.push('')
|
|
1287
|
-
lines.push(` ${chalk.bgRed.white.bold(' ⚠ EXPERIMENTAL ')} ${chalk.red('This feature is under active development and may not work as expected.')}`)
|
|
1288
|
-
lines.push(` ${chalk.red('Found a bug? Press')} ${chalk.bold.white('I')} ${chalk.red('on the main screen or join our Discord to report issues & suggest improvements.')}`)
|
|
1289
|
-
lines.push('')
|
|
1290
|
-
|
|
1291
|
-
// 📖 Feedback message (auto-clears after 5s)
|
|
1292
|
-
const msg = state.proxyDaemonMessage
|
|
1293
|
-
if (msg && (Date.now() - msg.ts < 5000)) {
|
|
1294
|
-
const msgColor = msg.type === 'success' ? chalk.greenBright : msg.type === 'warning' ? chalk.yellow : chalk.red
|
|
1295
|
-
lines.push(` ${msgColor(msg.msg)}`)
|
|
1296
|
-
lines.push('')
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
// ────────────────────────────── PROXY SECTION ──────────────────────────────
|
|
1300
|
-
lines.push(` ${chalk.bold('🔀 Proxy Configuration')}`)
|
|
1301
|
-
lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
|
|
1302
|
-
lines.push('')
|
|
1303
|
-
lines.push(` ${chalk.dim(' The local proxy groups all your provider API keys into a single')}`)
|
|
1304
|
-
lines.push(` ${chalk.dim(' endpoint. Tools like OpenCode, Claude Code, Goose, etc. connect')}`)
|
|
1305
|
-
lines.push(` ${chalk.dim(' to this proxy which handles key rotation, rate limiting, and failover.')}`)
|
|
1306
|
-
lines.push('')
|
|
1307
|
-
|
|
1308
|
-
// 📖 Proxy sync now always follows the currently selected Z-mode when supported.
|
|
1309
|
-
const currentToolMode = state.mode || 'opencode'
|
|
1310
|
-
const currentToolMeta = getToolMeta(currentToolMode)
|
|
1311
|
-
const currentToolLabel = `${currentToolMeta.emoji} ${currentToolMeta.label}`
|
|
1312
|
-
const proxySyncTool = resolveProxySyncToolMode(currentToolMode)
|
|
1313
|
-
const proxySyncHint = proxySyncTool
|
|
1314
|
-
? chalk.dim(` Current tool: ${currentToolLabel}`)
|
|
1315
|
-
: chalk.yellow(` Current tool: ${currentToolLabel} (launcher-only, no persisted proxy config)`)
|
|
1316
|
-
lines.push(proxySyncHint)
|
|
1317
|
-
lines.push('')
|
|
1318
|
-
|
|
1319
|
-
// 📖 Row 0: Proxy enabled toggle
|
|
1320
|
-
const r0b = state.proxyDaemonCursor === ROW_PROXY_ENABLED ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1321
|
-
const r0val = proxySettings.enabled ? chalk.greenBright('Enabled') : chalk.dim('Disabled (opt-in)')
|
|
1322
|
-
const r0 = `${r0b}${chalk.bold('Proxy mode').padEnd(44)} ${r0val}`
|
|
1323
|
-
cursorLineByRow[ROW_PROXY_ENABLED] = lines.length
|
|
1324
|
-
lines.push(state.proxyDaemonCursor === ROW_PROXY_ENABLED ? chalk.bgRgb(20, 45, 60)(r0) : r0)
|
|
1325
|
-
|
|
1326
|
-
// 📖 Row 1: Auto-sync proxy config to the current tool when that tool supports persisted sync.
|
|
1327
|
-
const r2b = state.proxyDaemonCursor === ROW_PROXY_SYNC ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1328
|
-
const r2val = proxySettings.syncToOpenCode ? chalk.greenBright('Enabled') : chalk.dim('Disabled')
|
|
1329
|
-
const r2label = proxySyncTool
|
|
1330
|
-
? `Auto-sync proxy to ${currentToolMeta.label}`
|
|
1331
|
-
: 'Auto-sync proxy to current tool'
|
|
1332
|
-
const r2note = proxySyncTool ? '' : ` ${chalk.dim('(unavailable for this mode)')}`
|
|
1333
|
-
const r2 = `${r2b}${chalk.bold(r2label).padEnd(44)} ${r2val}${r2note}`
|
|
1334
|
-
cursorLineByRow[ROW_PROXY_SYNC] = lines.length
|
|
1335
|
-
lines.push(state.proxyDaemonCursor === ROW_PROXY_SYNC ? chalk.bgRgb(20, 45, 60)(r2) : r2)
|
|
1336
|
-
|
|
1337
|
-
// 📖 Row 2: Preferred port
|
|
1338
|
-
const r3b = state.proxyDaemonCursor === ROW_PROXY_PORT ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1339
|
-
const r3val = state.settingsProxyPortEditMode && state.proxyDaemonCursor === ROW_PROXY_PORT
|
|
1340
|
-
? chalk.cyanBright(`${state.settingsProxyPortBuffer}▏`)
|
|
1341
|
-
: (proxySettings.preferredPort === 0 ? chalk.dim('auto (OS-assigned)') : chalk.green(String(proxySettings.preferredPort)))
|
|
1342
|
-
const r3 = `${r3b}${chalk.bold('Preferred proxy port').padEnd(44)} ${r3val}`
|
|
1343
|
-
cursorLineByRow[ROW_PROXY_PORT] = lines.length
|
|
1344
|
-
lines.push(state.proxyDaemonCursor === ROW_PROXY_PORT ? chalk.bgRgb(20, 45, 60)(r3) : r3)
|
|
1345
|
-
|
|
1346
|
-
// 📖 Row 3: Clean current tool proxy config
|
|
1347
|
-
const r4b = state.proxyDaemonCursor === ROW_PROXY_CLEANUP ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1348
|
-
const r4title = proxySyncTool
|
|
1349
|
-
? `Clean ${currentToolMeta.label} proxy config`
|
|
1350
|
-
: `Clean ${currentToolMeta.label} proxy config`
|
|
1351
|
-
const r4hint = proxySyncTool
|
|
1352
|
-
? chalk.dim('Enter → removes all fcm-* entries')
|
|
1353
|
-
: chalk.dim('Unavailable for this mode')
|
|
1354
|
-
const r4 = `${r4b}${chalk.bold(r4title).padEnd(44)} ${r4hint}`
|
|
1355
|
-
cursorLineByRow[ROW_PROXY_CLEANUP] = lines.length
|
|
1356
|
-
lines.push(state.proxyDaemonCursor === ROW_PROXY_CLEANUP ? chalk.bgRgb(45, 30, 30)(r4) : r4)
|
|
1357
|
-
|
|
1358
|
-
// ────────────────────────────── DAEMON SECTION ─────────────────────────────
|
|
1359
|
-
lines.push('')
|
|
1360
|
-
lines.push(` ${chalk.bold('📡 FCM Proxy V2 Background Service')}`)
|
|
1361
|
-
lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
|
|
1362
|
-
lines.push('')
|
|
1363
|
-
lines.push(` ${chalk.dim(' The background service keeps FCM Proxy V2 running 24/7 — even when')}`)
|
|
1364
|
-
lines.push(` ${chalk.dim(' the TUI is closed or after a reboot. Claude Code, Gemini CLI, and')}`)
|
|
1365
|
-
lines.push(` ${chalk.dim(' all tools stay connected at all times.')}`)
|
|
1366
|
-
lines.push('')
|
|
1367
|
-
|
|
1368
|
-
// 📖 Status display
|
|
1369
|
-
let daemonStatusLine = ` ${chalk.bold(' Status:')} `
|
|
1370
|
-
if (daemonStatus === 'running') {
|
|
1371
|
-
daemonStatusLine += chalk.greenBright('● Running')
|
|
1372
|
-
if (daemonInfo) daemonStatusLine += chalk.dim(` — PID ${daemonInfo.pid} • Port ${daemonInfo.port} • ${daemonInfo.accountCount || '?'} accounts • ${daemonInfo.modelCount || '?'} models`)
|
|
1373
|
-
} else if (daemonStatus === 'stopped') {
|
|
1374
|
-
daemonStatusLine += chalk.yellow('○ Stopped') + chalk.dim(' — service installed but not running')
|
|
1375
|
-
} else if (daemonStatus === 'stale') {
|
|
1376
|
-
daemonStatusLine += chalk.red('⚠ Stale') + chalk.dim(' — service crashed, PID no longer alive')
|
|
1377
|
-
} else if (daemonStatus === 'unhealthy') {
|
|
1378
|
-
daemonStatusLine += chalk.red('⚠ Unhealthy') + chalk.dim(' — PID alive but health check failed')
|
|
1379
|
-
} else {
|
|
1380
|
-
daemonStatusLine += chalk.dim('○ Not installed')
|
|
1381
|
-
}
|
|
1382
|
-
lines.push(daemonStatusLine)
|
|
1383
|
-
|
|
1384
|
-
// 📖 Version mismatch warning
|
|
1385
|
-
if (daemonInfo?.version && daemonInfo.version !== LOCAL_VERSION) {
|
|
1386
|
-
lines.push(` ${chalk.yellow(` ⚠ Version mismatch: service v${daemonInfo.version} vs FCM v${LOCAL_VERSION}`)}`)
|
|
1387
|
-
lines.push(` ${chalk.dim(' Restart or reinstall the service to apply the update.')}`)
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// 📖 Uptime
|
|
1391
|
-
if (daemonStatus === 'running' && daemonInfo?.startedAt) {
|
|
1392
|
-
const upSec = Math.floor((Date.now() - new Date(daemonInfo.startedAt).getTime()) / 1000)
|
|
1393
|
-
const upMin = Math.floor(upSec / 60)
|
|
1394
|
-
const upHr = Math.floor(upMin / 60)
|
|
1395
|
-
const uptimeStr = upHr > 0 ? `${upHr}h ${upMin % 60}m` : upMin > 0 ? `${upMin}m ${upSec % 60}s` : `${upSec}s`
|
|
1396
|
-
lines.push(` ${chalk.dim(` Uptime: ${uptimeStr}`)}`)
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
lines.push('')
|
|
1400
|
-
|
|
1401
|
-
// 📖 Row 5: Install / Uninstall
|
|
1402
|
-
const d0b = state.proxyDaemonCursor === ROW_DAEMON_INSTALL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1403
|
-
const d0label = daemonIsInstalled ? 'Uninstall service' : 'Install background service'
|
|
1404
|
-
const d0hint = daemonIsInstalled
|
|
1405
|
-
? chalk.dim('Enter → stop service + remove config')
|
|
1406
|
-
: chalk.dim('Enter → install as OS service (launchd/systemd)')
|
|
1407
|
-
const d0 = `${d0b}${chalk.bold(d0label).padEnd(44)} ${d0hint}`
|
|
1408
|
-
cursorLineByRow[ROW_DAEMON_INSTALL] = lines.length
|
|
1409
|
-
lines.push(state.proxyDaemonCursor === ROW_DAEMON_INSTALL ? chalk.bgRgb(daemonIsInstalled ? 45 : 20, daemonIsInstalled ? 30 : 45, daemonIsInstalled ? 30 : 40)(d0) : d0)
|
|
1410
|
-
|
|
1411
|
-
// 📖 Rows 6-9 only shown when service is installed
|
|
1412
|
-
if (daemonIsInstalled) {
|
|
1413
|
-
// 📖 Row 6: Restart
|
|
1414
|
-
const d1b = state.proxyDaemonCursor === ROW_DAEMON_RESTART ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1415
|
-
const d1 = `${d1b}${chalk.bold('Restart service').padEnd(44)} ${chalk.dim('Enter → stop + start via OS service manager')}`
|
|
1416
|
-
cursorLineByRow[ROW_DAEMON_RESTART] = lines.length
|
|
1417
|
-
lines.push(state.proxyDaemonCursor === ROW_DAEMON_RESTART ? chalk.bgRgb(20, 45, 60)(d1) : d1)
|
|
1418
|
-
|
|
1419
|
-
// 📖 Row 7: Stop (SIGTERM)
|
|
1420
|
-
const d2b = state.proxyDaemonCursor === ROW_DAEMON_STOP ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1421
|
-
const d2warn = chalk.dim(' (service may auto-restart)')
|
|
1422
|
-
const d2 = `${d2b}${chalk.bold('Stop service').padEnd(44)} ${chalk.dim('Enter → graceful shutdown (SIGTERM)')}${d2warn}`
|
|
1423
|
-
cursorLineByRow[ROW_DAEMON_STOP] = lines.length
|
|
1424
|
-
lines.push(state.proxyDaemonCursor === ROW_DAEMON_STOP ? chalk.bgRgb(45, 40, 20)(d2) : d2)
|
|
1425
|
-
|
|
1426
|
-
// 📖 Row 8: Force kill (SIGKILL) — emergency
|
|
1427
|
-
const d3b = state.proxyDaemonCursor === ROW_DAEMON_KILL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1428
|
-
const d3 = `${d3b}${chalk.bold.red('Force kill service').padEnd(44)} ${chalk.dim('Enter → SIGKILL — emergency only')}`
|
|
1429
|
-
cursorLineByRow[ROW_DAEMON_KILL] = lines.length
|
|
1430
|
-
lines.push(state.proxyDaemonCursor === ROW_DAEMON_KILL ? chalk.bgRgb(60, 20, 20)(d3) : d3)
|
|
1431
|
-
|
|
1432
|
-
// 📖 Row 9: View logs
|
|
1433
|
-
const d4b = state.proxyDaemonCursor === ROW_DAEMON_LOGS ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1434
|
-
const d4 = `${d4b}${chalk.bold('View service logs').padEnd(44)} ${chalk.dim('Enter → show last 50 log lines')}`
|
|
1435
|
-
cursorLineByRow[ROW_DAEMON_LOGS] = lines.length
|
|
1436
|
-
lines.push(state.proxyDaemonCursor === ROW_DAEMON_LOGS ? chalk.bgRgb(30, 30, 50)(d4) : d4)
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
// ────────────────────────────── INFO SECTION ───────────────────────────────
|
|
1440
|
-
lines.push('')
|
|
1441
|
-
lines.push(` ${chalk.bold('ℹ How it works')}`)
|
|
1442
|
-
lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
|
|
1443
|
-
lines.push('')
|
|
1444
|
-
lines.push(` ${chalk.dim(' 📖 The proxy starts a local HTTP server on 127.0.0.1 (localhost only).')}`)
|
|
1445
|
-
lines.push(` ${chalk.dim(' 📖 External tools connect to it as if it were OpenAI/Anthropic.')}`)
|
|
1446
|
-
lines.push(` ${chalk.dim(' 📖 The proxy rotates between your API keys across all providers.')}`)
|
|
1447
|
-
lines.push('')
|
|
1448
|
-
lines.push(` ${chalk.dim(' 📖 The background service adds persistence: install it once, and the proxy')}`)
|
|
1449
|
-
lines.push(` ${chalk.dim(' 📖 starts automatically at login and survives reboots.')}`)
|
|
1450
|
-
lines.push('')
|
|
1451
|
-
lines.push(` ${chalk.dim(' 📖 Claude Code support: FCM Proxy V2 translates Anthropic wire format')}`)
|
|
1452
|
-
lines.push(` ${chalk.dim(' 📖 (POST /v1/messages) to OpenAI format for upstream providers.')}`)
|
|
1453
|
-
lines.push('')
|
|
1454
|
-
if (process.platform === 'darwin') {
|
|
1455
|
-
lines.push(` ${chalk.dim(' 📦 macOS: launchd LaunchAgent at ~/Library/LaunchAgents/com.fcm.proxy.plist')}`)
|
|
1456
|
-
} else if (process.platform === 'linux') {
|
|
1457
|
-
lines.push(` ${chalk.dim(' 📦 Linux: systemd user service at ~/.config/systemd/user/fcm-proxy.service')}`)
|
|
1458
|
-
} else {
|
|
1459
|
-
lines.push(` ${chalk.dim(' ⚠ Windows: background service not supported — use in-process proxy (starts with TUI)')}`)
|
|
1460
|
-
}
|
|
1461
|
-
lines.push('')
|
|
1462
|
-
|
|
1463
|
-
// 📖 Clamp cursor
|
|
1464
|
-
if (state.proxyDaemonCursor > maxRow) state.proxyDaemonCursor = maxRow
|
|
1465
|
-
|
|
1466
|
-
// 📖 Scrolling and tinting
|
|
1467
|
-
const PROXY_DAEMON_BG = chalk.bgRgb(15, 25, 45)
|
|
1468
|
-
const targetLine = cursorLineByRow[state.proxyDaemonCursor] ?? 0
|
|
1469
|
-
state.proxyDaemonScrollOffset = keepOverlayTargetVisible(
|
|
1470
|
-
state.proxyDaemonScrollOffset,
|
|
1471
|
-
targetLine,
|
|
1472
|
-
lines.length,
|
|
1473
|
-
state.terminalRows
|
|
1474
|
-
)
|
|
1475
|
-
const { visible, offset } = sliceOverlayLines(lines, state.proxyDaemonScrollOffset, state.terminalRows)
|
|
1476
|
-
state.proxyDaemonScrollOffset = offset
|
|
1477
|
-
const tintedLines = tintOverlayLines(visible, PROXY_DAEMON_BG, state.terminalCols)
|
|
1478
|
-
return tintedLines.map(l => l + EL).join('\n')
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
959
|
// 📖 stopRecommendAnalysis: cleanup timers if user cancels during analysis
|
|
1482
960
|
function stopRecommendAnalysis() {
|
|
1483
961
|
if (state.recommendAnalysisTimer) { clearInterval(state.recommendAnalysisTimer); state.recommendAnalysisTimer = null }
|
|
@@ -1486,10 +964,8 @@ const profileStartIdx = updateRowIdx + 5
|
|
|
1486
964
|
|
|
1487
965
|
return {
|
|
1488
966
|
renderSettings,
|
|
1489
|
-
renderProxyDaemon,
|
|
1490
967
|
renderInstallEndpoints,
|
|
1491
968
|
renderHelp,
|
|
1492
|
-
renderLog,
|
|
1493
969
|
renderRecommend,
|
|
1494
970
|
renderFeedback,
|
|
1495
971
|
renderChangelog,
|