free-coding-models 0.3.11 → 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 +19 -0
- package/README.md +112 -1134
- package/bin/free-coding-models.js +13 -167
- package/package.json +2 -3
- package/src/cli-help.js +0 -18
- package/src/config.js +5 -117
- package/src/endpoint-installer.js +26 -64
- package/src/key-handler.js +56 -437
- 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 +23 -517
- package/src/product-flags.js +14 -0
- package/src/render-helpers.js +2 -34
- package/src/render-table.js +10 -18
- 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 +3 -68
- 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 -157
- package/src/log-reader.js +0 -195
- package/src/opencode-sync.js +0 -200
- package/src/proxy-foreground.js +0 -234
- package/src/proxy-server.js +0 -1506
- package/src/proxy-sync.js +0 -591
- 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,19 +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
36
|
TIER_CYCLE,
|
|
42
37
|
SETTINGS_OVERLAY_BG,
|
|
43
38
|
HELP_OVERLAY_BG,
|
|
44
39
|
RECOMMEND_OVERLAY_BG,
|
|
45
|
-
LOG_OVERLAY_BG,
|
|
46
40
|
OVERLAY_PANEL_WIDTH,
|
|
47
41
|
keepOverlayTargetVisible,
|
|
48
42
|
sliceOverlayLines,
|
|
49
43
|
tintOverlayLines,
|
|
50
|
-
loadRecentLogs,
|
|
51
44
|
TASK_TYPES,
|
|
52
45
|
PRIORITY_TYPES,
|
|
53
46
|
CONTEXT_BUDGETS,
|
|
@@ -62,7 +55,6 @@ export function createOverlayRenderers(state, deps) {
|
|
|
62
55
|
getConfiguredInstallableProviders,
|
|
63
56
|
getInstallTargetModes,
|
|
64
57
|
getProviderCatalogModels,
|
|
65
|
-
CONNECTION_MODES,
|
|
66
58
|
getToolMeta,
|
|
67
59
|
} = deps
|
|
68
60
|
|
|
@@ -89,54 +81,6 @@ export function createOverlayRenderers(state, deps) {
|
|
|
89
81
|
return lines
|
|
90
82
|
}
|
|
91
83
|
|
|
92
|
-
// 📖 Keep log token formatting aligned with the main table so the same totals
|
|
93
|
-
// 📖 read the same everywhere in the TUI.
|
|
94
|
-
const formatLogTokens = (totalTokens) => {
|
|
95
|
-
const safeTotal = Number(totalTokens) || 0
|
|
96
|
-
if (safeTotal <= 0) return '--'
|
|
97
|
-
if (safeTotal >= 999_500) return `${(safeTotal / 1_000_000).toFixed(2)}M`
|
|
98
|
-
if (safeTotal >= 1_000) return `${(safeTotal / 1_000).toFixed(2)}k`
|
|
99
|
-
return String(Math.floor(safeTotal))
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 📖 Colorize latency with gradient: green (<500ms) → orange (<1000ms) → yellow (<1500ms) → red (>=1500ms)
|
|
103
|
-
const colorizeLatency = (latency, text) => {
|
|
104
|
-
const ms = Number(latency) || 0
|
|
105
|
-
if (ms <= 0) return chalk.dim(text)
|
|
106
|
-
if (ms < 500) return chalk.greenBright(text)
|
|
107
|
-
if (ms < 1000) return chalk.rgb(255, 165, 0)(text) // Orange
|
|
108
|
-
if (ms < 1500) return chalk.yellow(text)
|
|
109
|
-
return chalk.red(text)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// 📖 Colorize tokens with gradient: dim green (few) → bright green (many)
|
|
113
|
-
const colorizeTokens = (tokens, text) => {
|
|
114
|
-
const tok = Number(tokens) || 0
|
|
115
|
-
if (tok <= 0) return chalk.dim(text)
|
|
116
|
-
// Gradient: light green (low) → medium green → bright green (high, >30k)
|
|
117
|
-
if (tok < 10_000) return chalk.hex('#90EE90')(text) // Light green
|
|
118
|
-
if (tok < 30_000) return chalk.hex('#32CD32')(text) // Lime green
|
|
119
|
-
return chalk.greenBright(text) // Full brightness green
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// 📖 Get model color based on status code - distinct colors for each error type
|
|
123
|
-
const getModelColorByStatus = (status) => {
|
|
124
|
-
const sc = String(status)
|
|
125
|
-
if (sc === '200') return chalk.greenBright // Success - bright green
|
|
126
|
-
if (sc === '404') return chalk.rgb(139, 0, 0) // Not found - dark red
|
|
127
|
-
if (sc === '400') return chalk.hex('#8B008B') // Bad request - dark magenta
|
|
128
|
-
if (sc === '401') return chalk.hex('#9932CC') // Unauthorized - dark orchid
|
|
129
|
-
if (sc === '403') return chalk.hex('#BA55D3') // Forbidden - medium orchid
|
|
130
|
-
if (sc === '413') return chalk.hex('#FF6347') // Payload too large - tomato red
|
|
131
|
-
if (sc === '429') return chalk.hex('#FFB90F') // Rate limit - dark orange
|
|
132
|
-
if (sc === '500') return chalk.hex('#DC143C') // Internal server error - crimson
|
|
133
|
-
if (sc === '502') return chalk.hex('#C71585') // Bad gateway - medium violet red
|
|
134
|
-
if (sc === '503') return chalk.hex('#9370DB') // Service unavailable - medium purple
|
|
135
|
-
if (sc.startsWith('5')) return chalk.magenta // Other 5xx - magenta
|
|
136
|
-
if (sc === '0') return chalk.hex('#696969') // Timeout/error - dim gray
|
|
137
|
-
return chalk.white // Unknown - white
|
|
138
|
-
}
|
|
139
|
-
|
|
140
84
|
// ─── Settings screen renderer ─────────────────────────────────────────────
|
|
141
85
|
// 📖 renderSettings: Draw the settings overlay in the alt screen buffer.
|
|
142
86
|
// 📖 Shows all providers with their API key (masked) + enabled state.
|
|
@@ -144,11 +88,10 @@ export function createOverlayRenderers(state, deps) {
|
|
|
144
88
|
// 📖 Key "T" in settings = test API key for selected provider.
|
|
145
89
|
function renderSettings() {
|
|
146
90
|
const providerKeys = Object.keys(sources)
|
|
147
|
-
const updateRowIdx = providerKeys.length
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
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
|
|
152
95
|
const EL = '\x1b[K'
|
|
153
96
|
const lines = []
|
|
154
97
|
const cursorLineByRow = {}
|
|
@@ -285,23 +228,11 @@ const updateRowIdx = providerKeys.length
|
|
|
285
228
|
lines.push(chalk.red(` ${state.settingsUpdateError}`))
|
|
286
229
|
}
|
|
287
230
|
|
|
288
|
-
// 📖
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
lines.push(
|
|
293
|
-
|
|
294
|
-
const proxyDaemonBullet = state.settingsCursor === proxyDaemonRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
295
|
-
const proxyStatus = proxySettings.enabled ? chalk.greenBright('Proxy ON') : chalk.dim('Proxy OFF')
|
|
296
|
-
const daemonStatus = state.daemonStatus || 'not-installed'
|
|
297
|
-
let daemonBadge
|
|
298
|
-
if (daemonStatus === 'running') daemonBadge = chalk.greenBright('Service ON')
|
|
299
|
-
else if (daemonStatus === 'stopped') daemonBadge = chalk.yellow('Service stopped')
|
|
300
|
-
else if (daemonStatus === 'stale' || daemonStatus === 'unhealthy') daemonBadge = chalk.red('Service ' + daemonStatus)
|
|
301
|
-
else daemonBadge = chalk.dim('Service OFF')
|
|
302
|
-
const proxyDaemonRow = `${proxyDaemonBullet}${chalk.bold('FCM Proxy V2 settings →').padEnd(44)} ${proxyStatus} ${chalk.dim('•')} ${daemonBadge}`
|
|
303
|
-
cursorLineByRow[proxyDaemonRowIdx] = lines.length
|
|
304
|
-
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)
|
|
305
236
|
|
|
306
237
|
// 📖 Changelog viewer row
|
|
307
238
|
const changelogViewBullet = state.settingsCursor === changelogViewRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
@@ -314,10 +245,8 @@ const updateRowIdx = providerKeys.length
|
|
|
314
245
|
lines.push('')
|
|
315
246
|
if (state.settingsEditMode) {
|
|
316
247
|
lines.push(chalk.dim(' Type API key • Enter Save • Esc Cancel'))
|
|
317
|
-
} else if (state.settingsProxyPortEditMode) {
|
|
318
|
-
lines.push(chalk.dim(' Type proxy port (0 = auto) • Enter Save • Esc Cancel'))
|
|
319
248
|
} else {
|
|
320
|
-
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'))
|
|
321
250
|
}
|
|
322
251
|
// 📖 Show sync/restore status message if set
|
|
323
252
|
if (state.settingsSyncStatus) {
|
|
@@ -355,7 +284,7 @@ const updateRowIdx = providerKeys.length
|
|
|
355
284
|
}
|
|
356
285
|
|
|
357
286
|
// ─── Install Endpoints overlay renderer ───────────────────────────────────
|
|
358
|
-
// 📖 renderInstallEndpoints drives the provider → tool →
|
|
287
|
+
// 📖 renderInstallEndpoints drives the provider → tool → scope → model flow
|
|
359
288
|
// 📖 behind the `Y` hotkey. It deliberately reuses the same overlay viewport
|
|
360
289
|
// 📖 helpers as Settings so long provider/model lists stay navigable.
|
|
361
290
|
function renderInstallEndpoints() {
|
|
@@ -364,8 +293,7 @@ const updateRowIdx = providerKeys.length
|
|
|
364
293
|
const cursorLineByRow = {}
|
|
365
294
|
const providerChoices = getConfiguredInstallableProviders(state.config)
|
|
366
295
|
const toolChoices = getInstallTargetModes()
|
|
367
|
-
const
|
|
368
|
-
const totalSteps = 5
|
|
296
|
+
const totalSteps = 4
|
|
369
297
|
const scopeChoices = [
|
|
370
298
|
{
|
|
371
299
|
key: 'all',
|
|
@@ -391,11 +319,7 @@ const updateRowIdx = providerKeys.length
|
|
|
391
319
|
})()
|
|
392
320
|
: '—'
|
|
393
321
|
|
|
394
|
-
const selectedConnectionLabel =
|
|
395
|
-
? 'FCM Proxy V2'
|
|
396
|
-
: state.installEndpointsConnectionMode === 'direct'
|
|
397
|
-
? 'Direct Provider'
|
|
398
|
-
: '—'
|
|
322
|
+
const selectedConnectionLabel = 'Direct Provider'
|
|
399
323
|
|
|
400
324
|
lines.push('')
|
|
401
325
|
// 📖 Branding header
|
|
@@ -439,7 +363,7 @@ const updateRowIdx = providerKeys.length
|
|
|
439
363
|
const label = `${meta.emoji} ${meta.label}`
|
|
440
364
|
const note = toolMode.startsWith('opencode')
|
|
441
365
|
? chalk.dim('shared config file')
|
|
442
|
-
:
|
|
366
|
+
: toolMode === 'openhands'
|
|
443
367
|
? chalk.dim('env file (~/.fcm-*-env)')
|
|
444
368
|
: chalk.dim('managed config install')
|
|
445
369
|
const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
@@ -450,26 +374,8 @@ const updateRowIdx = providerKeys.length
|
|
|
450
374
|
|
|
451
375
|
lines.push('')
|
|
452
376
|
lines.push(chalk.dim(' ↑↓ Navigate • Enter Choose tool • Esc Back'))
|
|
453
|
-
} else if (state.installEndpointsPhase === 'connection') {
|
|
454
|
-
// 📖 Step 3: Choose connection mode — Direct Provider vs FCM Proxy
|
|
455
|
-
lines.push(` ${chalk.bold(`Step 3/${totalSteps}`)} ${chalk.cyan('Choose connection mode')}`)
|
|
456
|
-
lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel}`))
|
|
457
|
-
lines.push('')
|
|
458
|
-
|
|
459
|
-
connectionChoices.forEach((mode, idx) => {
|
|
460
|
-
const isCursor = idx === state.installEndpointsCursor
|
|
461
|
-
const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
462
|
-
const icon = mode.key === 'proxy' ? '🔄' : '⚡'
|
|
463
|
-
const row = `${bullet}${icon} ${chalk.bold(mode.label)}`
|
|
464
|
-
cursorLineByRow[idx] = lines.length
|
|
465
|
-
lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
|
|
466
|
-
lines.push(chalk.dim(` ${mode.hint}`))
|
|
467
|
-
lines.push('')
|
|
468
|
-
})
|
|
469
|
-
|
|
470
|
-
lines.push(chalk.dim(' Enter Continue • Esc Back'))
|
|
471
377
|
} else if (state.installEndpointsPhase === 'scope') {
|
|
472
|
-
lines.push(` ${chalk.bold(`Step
|
|
378
|
+
lines.push(` ${chalk.bold(`Step 3/${totalSteps}`)} ${chalk.cyan('Choose the install scope')}`)
|
|
473
379
|
lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel} • ${selectedConnectionLabel}`))
|
|
474
380
|
lines.push('')
|
|
475
381
|
|
|
@@ -488,7 +394,7 @@ const updateRowIdx = providerKeys.length
|
|
|
488
394
|
const models = getProviderCatalogModels(state.installEndpointsProviderKey)
|
|
489
395
|
const selectedCount = state.installEndpointsSelectedModelIds.size
|
|
490
396
|
|
|
491
|
-
lines.push(` ${chalk.bold(`Step
|
|
397
|
+
lines.push(` ${chalk.bold(`Step 4/${totalSteps}`)} ${chalk.cyan('Choose which models to install')}`)
|
|
492
398
|
lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel} • ${selectedConnectionLabel}`))
|
|
493
399
|
lines.push(chalk.dim(` Selected: ${selectedCount}/${models.length}`))
|
|
494
400
|
lines.push('')
|
|
@@ -590,8 +496,8 @@ const updateRowIdx = providerKeys.length
|
|
|
590
496
|
lines.push(` ${chalk.cyan('Up%')} Uptime — ratio of successful pings to total pings ${chalk.dim('Sort:')} ${chalk.yellow('U')}`)
|
|
591
497
|
lines.push(` ${chalk.dim('If a model only works half the time, you\'ll waste time retrying. Higher = more reliable.')}`)
|
|
592
498
|
lines.push('')
|
|
593
|
-
lines.push(` ${chalk.cyan('Used')}
|
|
594
|
-
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.')}`)
|
|
595
501
|
lines.push('')
|
|
596
502
|
|
|
597
503
|
|
|
@@ -604,14 +510,12 @@ const updateRowIdx = providerKeys.length
|
|
|
604
510
|
lines.push(` ${chalk.bold('Controls')}`)
|
|
605
511
|
lines.push(` ${chalk.yellow('W')} Toggle ping mode ${chalk.dim('(speed 2s → normal 10s → slow 30s → forced 4s)')}`)
|
|
606
512
|
lines.push(` ${chalk.yellow('E')} Toggle configured models only ${chalk.dim('(enabled by default)')}`)
|
|
607
|
-
lines.push(` ${chalk.yellow('
|
|
608
|
-
lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Claude Code → Codex → Gemini → Qwen → OpenHands → Amp)')}`)
|
|
513
|
+
lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Qwen → OpenHands → Amp)')}`)
|
|
609
514
|
lines.push(` ${chalk.yellow('F')} Toggle favorite on selected row ${chalk.dim('(⭐ pinned at top, persisted)')}`)
|
|
610
|
-
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)')}`)
|
|
611
516
|
lines.push(` ${chalk.yellow('Q')} Smart Recommend ${chalk.dim('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
|
|
612
517
|
lines.push(` ${chalk.rgb(255, 87, 51).bold('I')} Feedback, bugs & requests ${chalk.dim('(📝 send anonymous feedback, bug reports, or feature requests)')}`)
|
|
613
|
-
lines.push(` ${chalk.yellow('
|
|
614
|
-
lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, proxy, manual update)')}`)
|
|
518
|
+
lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, updates, legacy cleanup)')}`)
|
|
615
519
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
616
520
|
lines.push(` ${chalk.yellow('Shift+R')} Reset view settings ${chalk.dim('(tier filter, sort, provider filter → defaults)')}`)
|
|
617
521
|
lines.push(` ${chalk.yellow('N')} Changelog ${chalk.dim('(📋 browse all versions, Enter to view details)')}`)
|
|
@@ -622,7 +526,7 @@ const updateRowIdx = providerKeys.length
|
|
|
622
526
|
lines.push(` ${chalk.yellow('↑↓')} Navigate rows`)
|
|
623
527
|
lines.push(` ${chalk.yellow('PgUp/PgDn')} Jump by page`)
|
|
624
528
|
lines.push(` ${chalk.yellow('Home/End')} Jump first/last row`)
|
|
625
|
-
lines.push(` ${chalk.yellow('Enter')} Edit key /
|
|
529
|
+
lines.push(` ${chalk.yellow('Enter')} Edit key / run selected maintenance action`)
|
|
626
530
|
lines.push(` ${chalk.yellow('Space')} Toggle provider enable/disable`)
|
|
627
531
|
lines.push(` ${chalk.yellow('T')} Test selected provider key`)
|
|
628
532
|
lines.push(` ${chalk.yellow('U')} Check updates manually`)
|
|
@@ -638,173 +542,6 @@ const updateRowIdx = providerKeys.length
|
|
|
638
542
|
return cleared.join('\n')
|
|
639
543
|
}
|
|
640
544
|
|
|
641
|
-
// ─── Log page overlay renderer ────────────────────────────────────────────
|
|
642
|
-
// 📖 renderLog: Draw the log page overlay showing recent requests from
|
|
643
|
-
// 📖 ~/.free-coding-models/request-log.jsonl, newest-first.
|
|
644
|
-
// 📖 Toggled with X key. Esc or X closes.
|
|
645
|
-
function renderLog() {
|
|
646
|
-
const EL = '\x1b[K'
|
|
647
|
-
const lines = []
|
|
648
|
-
|
|
649
|
-
// 📖 Branding header
|
|
650
|
-
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
|
|
651
|
-
lines.push(` ${chalk.bold('📋 Request Log')}`)
|
|
652
|
-
lines.push('')
|
|
653
|
-
lines.push(chalk.dim(' — recent requests • ↑↓ scroll • A toggle all/500 • X or Esc close'))
|
|
654
|
-
lines.push(chalk.dim(' Works only when the multi-account proxy is enabled and requests go through it.'))
|
|
655
|
-
lines.push(chalk.dim(' Direct provider launches do not currently write into this log.'))
|
|
656
|
-
|
|
657
|
-
// 📖 Load recent log entries — bounded read, newest-first, malformed lines skipped.
|
|
658
|
-
// 📖 Show up to 500 entries by default, or all if logShowAll is true.
|
|
659
|
-
const logLimit = state.logShowAll ? Number.MAX_SAFE_INTEGER : 500
|
|
660
|
-
const logRows = loadRecentLogs({ limit: logLimit })
|
|
661
|
-
const totalTokens = logRows.reduce((sum, row) => sum + (Number(row.tokens) || 0), 0)
|
|
662
|
-
|
|
663
|
-
if (logRows.length === 0) {
|
|
664
|
-
lines.push(chalk.dim(' No log entries found.'))
|
|
665
|
-
lines.push(chalk.dim(' Logs are written to ~/.free-coding-models/request-log.jsonl'))
|
|
666
|
-
lines.push(chalk.dim(' when requests are proxied through the multi-account rotation proxy.'))
|
|
667
|
-
lines.push(chalk.dim(' Direct provider launches do not currently feed this token log.'))
|
|
668
|
-
} else {
|
|
669
|
-
lines.push(` ${chalk.bold('Total Consumed:')} ${chalk.greenBright(formatLogTokens(totalTokens))}`)
|
|
670
|
-
lines.push('')
|
|
671
|
-
// 📖 Column widths for the log table
|
|
672
|
-
const W_TIME = 19
|
|
673
|
-
const W_PROV = 14
|
|
674
|
-
const W_MODEL = 44
|
|
675
|
-
const W_ROUTE = 18
|
|
676
|
-
const W_STATUS = 8
|
|
677
|
-
const W_TOKENS = 12
|
|
678
|
-
const W_LAT = 10
|
|
679
|
-
|
|
680
|
-
// 📖 Header row
|
|
681
|
-
const hTime = chalk.dim('Time'.padEnd(W_TIME))
|
|
682
|
-
const hProv = chalk.dim('Provider'.padEnd(W_PROV))
|
|
683
|
-
const hModel = chalk.dim('Model'.padEnd(W_MODEL))
|
|
684
|
-
const hRoute = chalk.dim('Route'.padEnd(W_ROUTE))
|
|
685
|
-
const hStatus = chalk.dim('Status'.padEnd(W_STATUS))
|
|
686
|
-
const hTok = chalk.dim('Tokens Used'.padEnd(W_TOKENS))
|
|
687
|
-
const hLat = chalk.dim('Latency'.padEnd(W_LAT))
|
|
688
|
-
|
|
689
|
-
// 📖 Show mode indicator (all vs limited)
|
|
690
|
-
const modeBadge = state.logShowAll
|
|
691
|
-
? chalk.yellow.bold('ALL')
|
|
692
|
-
: chalk.cyan.bold('500')
|
|
693
|
-
const countBadge = chalk.dim(`Showing ${logRows.length} entries`)
|
|
694
|
-
|
|
695
|
-
lines.push(` ${hTime} ${hProv} ${hModel} ${hRoute} ${hStatus} ${hTok} ${hLat}`)
|
|
696
|
-
lines.push(` ${chalk.dim('─'.repeat(W_TIME + W_PROV + W_MODEL + W_ROUTE + W_STATUS + W_TOKENS + W_LAT + 12))} ${modeBadge} ${countBadge}`)
|
|
697
|
-
|
|
698
|
-
for (const row of logRows) {
|
|
699
|
-
// 📖 Format time as HH:MM:SS (strip the date part for compactness)
|
|
700
|
-
let timeStr = row.time
|
|
701
|
-
try {
|
|
702
|
-
const d = new Date(row.time)
|
|
703
|
-
if (!Number.isNaN(d.getTime())) {
|
|
704
|
-
timeStr = d.toISOString().replace('T', ' ').slice(0, 19)
|
|
705
|
-
}
|
|
706
|
-
} catch { /* keep raw */ }
|
|
707
|
-
|
|
708
|
-
const requestedModelLabel = row.requestedModel || ''
|
|
709
|
-
// 📖 Always show "requested → actual" if they differ, not just when switched
|
|
710
|
-
const displayModel = requestedModelLabel && requestedModelLabel !== row.model
|
|
711
|
-
? `${requestedModelLabel} → ${row.model}`
|
|
712
|
-
: row.model
|
|
713
|
-
|
|
714
|
-
// 📖 Color-code status with distinct colors for each error type
|
|
715
|
-
let statusCell
|
|
716
|
-
const sc = String(row.status)
|
|
717
|
-
if (sc === '200') {
|
|
718
|
-
statusCell = chalk.greenBright(sc.padEnd(W_STATUS))
|
|
719
|
-
} else if (sc === '404') {
|
|
720
|
-
statusCell = chalk.rgb(139, 0, 0).bold(sc.padEnd(W_STATUS)) // Dark red for 404
|
|
721
|
-
} else if (sc === '400') {
|
|
722
|
-
statusCell = chalk.hex('#8B008B').bold(sc.padEnd(W_STATUS)) // Dark magenta
|
|
723
|
-
} else if (sc === '401') {
|
|
724
|
-
statusCell = chalk.hex('#9932CC').bold(sc.padEnd(W_STATUS)) // Dark orchid
|
|
725
|
-
} else if (sc === '403') {
|
|
726
|
-
statusCell = chalk.hex('#BA55D3').bold(sc.padEnd(W_STATUS)) // Medium orchid
|
|
727
|
-
} else if (sc === '413') {
|
|
728
|
-
statusCell = chalk.hex('#FF6347').bold(sc.padEnd(W_STATUS)) // Tomato red
|
|
729
|
-
} else if (sc === '429') {
|
|
730
|
-
statusCell = chalk.hex('#FFB90F').bold(sc.padEnd(W_STATUS)) // Dark orange
|
|
731
|
-
} else if (sc === '500') {
|
|
732
|
-
statusCell = chalk.hex('#DC143C').bold(sc.padEnd(W_STATUS)) // Crimson
|
|
733
|
-
} else if (sc === '502') {
|
|
734
|
-
statusCell = chalk.hex('#C71585').bold(sc.padEnd(W_STATUS)) // Medium violet red
|
|
735
|
-
} else if (sc === '503') {
|
|
736
|
-
statusCell = chalk.hex('#9370DB').bold(sc.padEnd(W_STATUS)) // Medium purple
|
|
737
|
-
} else if (sc.startsWith('5')) {
|
|
738
|
-
statusCell = chalk.magenta(sc.padEnd(W_STATUS)) // Other 5xx - magenta
|
|
739
|
-
} else if (sc === '0') {
|
|
740
|
-
statusCell = chalk.hex('#696969')(sc.padEnd(W_STATUS)) // Dim gray for timeout
|
|
741
|
-
} else {
|
|
742
|
-
statusCell = chalk.dim(sc.padEnd(W_STATUS))
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
const tokStr = formatLogTokens(row.tokens)
|
|
746
|
-
const latStr = row.latency > 0 ? `${row.latency}ms` : '--'
|
|
747
|
-
const routeLabel = row.switched
|
|
748
|
-
? `SWITCHED ↻ ${row.switchReason || 'fallback'}`
|
|
749
|
-
: 'direct'
|
|
750
|
-
|
|
751
|
-
// 📖 Detect failed requests with zero tokens - these get special red highlighting
|
|
752
|
-
const isFailedWithZeroTokens = row.status !== '200' && (!row.tokens || Number(row.tokens) === 0)
|
|
753
|
-
|
|
754
|
-
const timeCell = chalk.dim(timeStr.slice(0, W_TIME).padEnd(W_TIME))
|
|
755
|
-
// 📖 Provider display: Use pretty label if available, otherwise raw key.
|
|
756
|
-
// 📖 All these logs are from FCM Proxy V2.
|
|
757
|
-
const providerLabel = PROVIDER_METADATA[row.provider]?.label || row.provider
|
|
758
|
-
const providerRgb = PROVIDER_COLOR[row.provider] ?? [105, 190, 245]
|
|
759
|
-
const provCell = chalk.bold.rgb(...providerRgb)(providerLabel.slice(0, W_PROV).padEnd(W_PROV))
|
|
760
|
-
|
|
761
|
-
// 📖 Color model based on status - red for failed requests with zero tokens
|
|
762
|
-
let modelCell
|
|
763
|
-
if (isFailedWithZeroTokens) {
|
|
764
|
-
modelCell = chalk.red.bold(displayModel.slice(0, W_MODEL).padEnd(W_MODEL))
|
|
765
|
-
} else {
|
|
766
|
-
const modelColorFn = getModelColorByStatus(row.status)
|
|
767
|
-
modelCell = row.switched
|
|
768
|
-
? chalk.bold.rgb(255, 210, 90)(displayModel.slice(0, W_MODEL).padEnd(W_MODEL))
|
|
769
|
-
: modelColorFn(displayModel.slice(0, W_MODEL).padEnd(W_MODEL))
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
const routeCell = row.switched
|
|
773
|
-
? chalk.bgRgb(120, 25, 25).yellow.bold(` ${routeLabel.slice(0, W_ROUTE - 2).padEnd(W_ROUTE - 2)} `)
|
|
774
|
-
: chalk.dim(routeLabel.padEnd(W_ROUTE))
|
|
775
|
-
|
|
776
|
-
// 📖 Colorize tokens - red cross emoji for failed requests with zero tokens
|
|
777
|
-
let tokCell
|
|
778
|
-
if (isFailedWithZeroTokens) {
|
|
779
|
-
tokCell = chalk.red.bold('✗'.padEnd(W_TOKENS))
|
|
780
|
-
} else {
|
|
781
|
-
tokCell = colorizeTokens(row.tokens, tokStr.padEnd(W_TOKENS))
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// 📖 Colorize latency with gradient (green → orange → yellow → red)
|
|
785
|
-
const latCell = colorizeLatency(row.latency, latStr.padEnd(W_LAT))
|
|
786
|
-
|
|
787
|
-
// 📖 Build the row line - add dark red background for failed requests with zero tokens
|
|
788
|
-
const rowText = ` ${timeCell} ${provCell} ${modelCell} ${routeCell} ${statusCell} ${tokCell} ${latCell}`
|
|
789
|
-
if (isFailedWithZeroTokens) {
|
|
790
|
-
lines.push(chalk.bgRgb(40, 0, 0)(rowText))
|
|
791
|
-
} else {
|
|
792
|
-
lines.push(rowText)
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
lines.push('')
|
|
798
|
-
lines.push(chalk.dim(` Showing up to 200 most recent entries • X or Esc close`))
|
|
799
|
-
lines.push('')
|
|
800
|
-
|
|
801
|
-
const { visible, offset } = sliceOverlayLines(lines, state.logScrollOffset, state.terminalRows)
|
|
802
|
-
state.logScrollOffset = offset
|
|
803
|
-
const tintedLines = tintOverlayLines(visible, LOG_OVERLAY_BG, state.terminalCols)
|
|
804
|
-
const cleared = tintedLines.map(l => l + EL)
|
|
805
|
-
return cleared.join('\n')
|
|
806
|
-
}
|
|
807
|
-
|
|
808
545
|
// 📖 renderRecommend: Draw the Smart Recommend overlay with 3 phases:
|
|
809
546
|
// 1. 'questionnaire' — ask 3 questions (task type, priority, context budget)
|
|
810
547
|
// 2. 'analyzing' — loading screen with progress bar (10s, 2 pings/sec)
|
|
@@ -1219,235 +956,6 @@ const updateRowIdx = providerKeys.length
|
|
|
1219
956
|
return cleared.join('\n')
|
|
1220
957
|
}
|
|
1221
958
|
|
|
1222
|
-
// ─── FCM Proxy V2 overlay renderer ──────────────────────────────────────────
|
|
1223
|
-
// 📖 renderProxyDaemon: Dedicated full-page overlay for FCM Proxy V2 configuration
|
|
1224
|
-
// 📖 and background service management. Opened from Settings → "FCM Proxy V2 settings →".
|
|
1225
|
-
// 📖 Contains all proxy toggles, service status/actions, explanations, and emergency kill.
|
|
1226
|
-
function renderProxyDaemon() {
|
|
1227
|
-
const EL = '\x1b[K'
|
|
1228
|
-
const lines = []
|
|
1229
|
-
const cursorLineByRow = {}
|
|
1230
|
-
const proxySettings = getProxySettings(state.config)
|
|
1231
|
-
|
|
1232
|
-
// 📖 Row indices — these control cursor navigation
|
|
1233
|
-
const ROW_PROXY_ENABLED = 0
|
|
1234
|
-
const ROW_PROXY_SYNC = 1
|
|
1235
|
-
const ROW_PROXY_PORT = 2
|
|
1236
|
-
const ROW_PROXY_CLEANUP = 3
|
|
1237
|
-
const ROW_DAEMON_INSTALL = 4
|
|
1238
|
-
const ROW_DAEMON_RESTART = 5
|
|
1239
|
-
const ROW_DAEMON_STOP = 6
|
|
1240
|
-
const ROW_DAEMON_KILL = 7
|
|
1241
|
-
const ROW_DAEMON_LOGS = 8
|
|
1242
|
-
|
|
1243
|
-
const daemonStatus = state.daemonStatus || 'not-installed'
|
|
1244
|
-
const daemonInfo = state.daemonInfo
|
|
1245
|
-
const daemonIsActive = daemonStatus === 'running' || daemonStatus === 'unhealthy' || daemonStatus === 'stale'
|
|
1246
|
-
const daemonIsInstalled = daemonIsActive || daemonStatus === 'stopped'
|
|
1247
|
-
|
|
1248
|
-
// 📖 Compute max row — hide daemon action rows when daemon not installed
|
|
1249
|
-
let maxRow = ROW_DAEMON_INSTALL
|
|
1250
|
-
if (daemonIsInstalled) maxRow = ROW_DAEMON_LOGS
|
|
1251
|
-
|
|
1252
|
-
// 📖 Header
|
|
1253
|
-
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
|
|
1254
|
-
lines.push(` ${chalk.bold('📡 FCM Proxy V2 Manager')}`)
|
|
1255
|
-
lines.push(` ${chalk.dim('— Esc back to Settings • ↑↓ navigate • Enter select')}`)
|
|
1256
|
-
lines.push('')
|
|
1257
|
-
lines.push(` ${chalk.bgRed.white.bold(' ⚠ EXPERIMENTAL ')} ${chalk.red('This feature is under active development and may not work as expected.')}`)
|
|
1258
|
-
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.')}`)
|
|
1259
|
-
lines.push('')
|
|
1260
|
-
|
|
1261
|
-
// 📖 Feedback message (auto-clears after 5s)
|
|
1262
|
-
const msg = state.proxyDaemonMessage
|
|
1263
|
-
if (msg && (Date.now() - msg.ts < 5000)) {
|
|
1264
|
-
const msgColor = msg.type === 'success' ? chalk.greenBright : msg.type === 'warning' ? chalk.yellow : chalk.red
|
|
1265
|
-
lines.push(` ${msgColor(msg.msg)}`)
|
|
1266
|
-
lines.push('')
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
// ────────────────────────────── PROXY SECTION ──────────────────────────────
|
|
1270
|
-
lines.push(` ${chalk.bold('🔀 Proxy Configuration')}`)
|
|
1271
|
-
lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
|
|
1272
|
-
lines.push('')
|
|
1273
|
-
lines.push(` ${chalk.dim(' The local proxy groups all your provider API keys into a single')}`)
|
|
1274
|
-
lines.push(` ${chalk.dim(' endpoint. Tools like OpenCode, Claude Code, Goose, etc. connect')}`)
|
|
1275
|
-
lines.push(` ${chalk.dim(' to this proxy which handles key rotation, rate limiting, and failover.')}`)
|
|
1276
|
-
lines.push('')
|
|
1277
|
-
|
|
1278
|
-
// 📖 Proxy sync now always follows the currently selected Z-mode when supported.
|
|
1279
|
-
const currentToolMode = state.mode || 'opencode'
|
|
1280
|
-
const currentToolMeta = getToolMeta(currentToolMode)
|
|
1281
|
-
const currentToolLabel = `${currentToolMeta.emoji} ${currentToolMeta.label}`
|
|
1282
|
-
const proxySyncTool = resolveProxySyncToolMode(currentToolMode)
|
|
1283
|
-
const proxySyncHint = proxySyncTool
|
|
1284
|
-
? chalk.dim(` Current tool: ${currentToolLabel}`)
|
|
1285
|
-
: chalk.yellow(` Current tool: ${currentToolLabel} (launcher-only, no persisted proxy config)`)
|
|
1286
|
-
lines.push(proxySyncHint)
|
|
1287
|
-
lines.push('')
|
|
1288
|
-
|
|
1289
|
-
// 📖 Row 0: Proxy enabled toggle
|
|
1290
|
-
const r0b = state.proxyDaemonCursor === ROW_PROXY_ENABLED ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1291
|
-
const r0val = proxySettings.enabled ? chalk.greenBright('Enabled') : chalk.dim('Disabled (opt-in)')
|
|
1292
|
-
const r0 = `${r0b}${chalk.bold('Proxy mode').padEnd(44)} ${r0val}`
|
|
1293
|
-
cursorLineByRow[ROW_PROXY_ENABLED] = lines.length
|
|
1294
|
-
lines.push(state.proxyDaemonCursor === ROW_PROXY_ENABLED ? chalk.bgRgb(20, 45, 60)(r0) : r0)
|
|
1295
|
-
|
|
1296
|
-
// 📖 Row 1: Auto-sync proxy config to the current tool when that tool supports persisted sync.
|
|
1297
|
-
const r2b = state.proxyDaemonCursor === ROW_PROXY_SYNC ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1298
|
-
const r2val = proxySettings.syncToOpenCode ? chalk.greenBright('Enabled') : chalk.dim('Disabled')
|
|
1299
|
-
const r2label = proxySyncTool
|
|
1300
|
-
? `Auto-sync proxy to ${currentToolMeta.label}`
|
|
1301
|
-
: 'Auto-sync proxy to current tool'
|
|
1302
|
-
const r2note = proxySyncTool ? '' : ` ${chalk.dim('(unavailable for this mode)')}`
|
|
1303
|
-
const r2 = `${r2b}${chalk.bold(r2label).padEnd(44)} ${r2val}${r2note}`
|
|
1304
|
-
cursorLineByRow[ROW_PROXY_SYNC] = lines.length
|
|
1305
|
-
lines.push(state.proxyDaemonCursor === ROW_PROXY_SYNC ? chalk.bgRgb(20, 45, 60)(r2) : r2)
|
|
1306
|
-
|
|
1307
|
-
// 📖 Row 2: Preferred port
|
|
1308
|
-
const r3b = state.proxyDaemonCursor === ROW_PROXY_PORT ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1309
|
-
const r3val = state.settingsProxyPortEditMode && state.proxyDaemonCursor === ROW_PROXY_PORT
|
|
1310
|
-
? chalk.cyanBright(`${state.settingsProxyPortBuffer}▏`)
|
|
1311
|
-
: (proxySettings.preferredPort === 0 ? chalk.dim('auto (OS-assigned)') : chalk.green(String(proxySettings.preferredPort)))
|
|
1312
|
-
const r3 = `${r3b}${chalk.bold('Preferred proxy port').padEnd(44)} ${r3val}`
|
|
1313
|
-
cursorLineByRow[ROW_PROXY_PORT] = lines.length
|
|
1314
|
-
lines.push(state.proxyDaemonCursor === ROW_PROXY_PORT ? chalk.bgRgb(20, 45, 60)(r3) : r3)
|
|
1315
|
-
|
|
1316
|
-
// 📖 Row 3: Clean current tool proxy config
|
|
1317
|
-
const r4b = state.proxyDaemonCursor === ROW_PROXY_CLEANUP ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1318
|
-
const r4title = proxySyncTool
|
|
1319
|
-
? `Clean ${currentToolMeta.label} proxy config`
|
|
1320
|
-
: `Clean ${currentToolMeta.label} proxy config`
|
|
1321
|
-
const r4hint = proxySyncTool
|
|
1322
|
-
? chalk.dim('Enter → removes all fcm-* entries')
|
|
1323
|
-
: chalk.dim('Unavailable for this mode')
|
|
1324
|
-
const r4 = `${r4b}${chalk.bold(r4title).padEnd(44)} ${r4hint}`
|
|
1325
|
-
cursorLineByRow[ROW_PROXY_CLEANUP] = lines.length
|
|
1326
|
-
lines.push(state.proxyDaemonCursor === ROW_PROXY_CLEANUP ? chalk.bgRgb(45, 30, 30)(r4) : r4)
|
|
1327
|
-
|
|
1328
|
-
// ────────────────────────────── DAEMON SECTION ─────────────────────────────
|
|
1329
|
-
lines.push('')
|
|
1330
|
-
lines.push(` ${chalk.bold('📡 FCM Proxy V2 Background Service')}`)
|
|
1331
|
-
lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
|
|
1332
|
-
lines.push('')
|
|
1333
|
-
lines.push(` ${chalk.dim(' The background service keeps FCM Proxy V2 running 24/7 — even when')}`)
|
|
1334
|
-
lines.push(` ${chalk.dim(' the TUI is closed or after a reboot. Claude Code, Gemini CLI, and')}`)
|
|
1335
|
-
lines.push(` ${chalk.dim(' all tools stay connected at all times.')}`)
|
|
1336
|
-
lines.push('')
|
|
1337
|
-
|
|
1338
|
-
// 📖 Status display
|
|
1339
|
-
let daemonStatusLine = ` ${chalk.bold(' Status:')} `
|
|
1340
|
-
if (daemonStatus === 'running') {
|
|
1341
|
-
daemonStatusLine += chalk.greenBright('● Running')
|
|
1342
|
-
if (daemonInfo) daemonStatusLine += chalk.dim(` — PID ${daemonInfo.pid} • Port ${daemonInfo.port} • ${daemonInfo.accountCount || '?'} accounts • ${daemonInfo.modelCount || '?'} models`)
|
|
1343
|
-
} else if (daemonStatus === 'stopped') {
|
|
1344
|
-
daemonStatusLine += chalk.yellow('○ Stopped') + chalk.dim(' — service installed but not running')
|
|
1345
|
-
} else if (daemonStatus === 'stale') {
|
|
1346
|
-
daemonStatusLine += chalk.red('⚠ Stale') + chalk.dim(' — service crashed, PID no longer alive')
|
|
1347
|
-
} else if (daemonStatus === 'unhealthy') {
|
|
1348
|
-
daemonStatusLine += chalk.red('⚠ Unhealthy') + chalk.dim(' — PID alive but health check failed')
|
|
1349
|
-
} else {
|
|
1350
|
-
daemonStatusLine += chalk.dim('○ Not installed')
|
|
1351
|
-
}
|
|
1352
|
-
lines.push(daemonStatusLine)
|
|
1353
|
-
|
|
1354
|
-
// 📖 Version mismatch warning
|
|
1355
|
-
if (daemonInfo?.version && daemonInfo.version !== LOCAL_VERSION) {
|
|
1356
|
-
lines.push(` ${chalk.yellow(` ⚠ Version mismatch: service v${daemonInfo.version} vs FCM v${LOCAL_VERSION}`)}`)
|
|
1357
|
-
lines.push(` ${chalk.dim(' Restart or reinstall the service to apply the update.')}`)
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
// 📖 Uptime
|
|
1361
|
-
if (daemonStatus === 'running' && daemonInfo?.startedAt) {
|
|
1362
|
-
const upSec = Math.floor((Date.now() - new Date(daemonInfo.startedAt).getTime()) / 1000)
|
|
1363
|
-
const upMin = Math.floor(upSec / 60)
|
|
1364
|
-
const upHr = Math.floor(upMin / 60)
|
|
1365
|
-
const uptimeStr = upHr > 0 ? `${upHr}h ${upMin % 60}m` : upMin > 0 ? `${upMin}m ${upSec % 60}s` : `${upSec}s`
|
|
1366
|
-
lines.push(` ${chalk.dim(` Uptime: ${uptimeStr}`)}`)
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
lines.push('')
|
|
1370
|
-
|
|
1371
|
-
// 📖 Row 5: Install / Uninstall
|
|
1372
|
-
const d0b = state.proxyDaemonCursor === ROW_DAEMON_INSTALL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1373
|
-
const d0label = daemonIsInstalled ? 'Uninstall service' : 'Install background service'
|
|
1374
|
-
const d0hint = daemonIsInstalled
|
|
1375
|
-
? chalk.dim('Enter → stop service + remove config')
|
|
1376
|
-
: chalk.dim('Enter → install as OS service (launchd/systemd)')
|
|
1377
|
-
const d0 = `${d0b}${chalk.bold(d0label).padEnd(44)} ${d0hint}`
|
|
1378
|
-
cursorLineByRow[ROW_DAEMON_INSTALL] = lines.length
|
|
1379
|
-
lines.push(state.proxyDaemonCursor === ROW_DAEMON_INSTALL ? chalk.bgRgb(daemonIsInstalled ? 45 : 20, daemonIsInstalled ? 30 : 45, daemonIsInstalled ? 30 : 40)(d0) : d0)
|
|
1380
|
-
|
|
1381
|
-
// 📖 Rows 6-9 only shown when service is installed
|
|
1382
|
-
if (daemonIsInstalled) {
|
|
1383
|
-
// 📖 Row 6: Restart
|
|
1384
|
-
const d1b = state.proxyDaemonCursor === ROW_DAEMON_RESTART ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1385
|
-
const d1 = `${d1b}${chalk.bold('Restart service').padEnd(44)} ${chalk.dim('Enter → stop + start via OS service manager')}`
|
|
1386
|
-
cursorLineByRow[ROW_DAEMON_RESTART] = lines.length
|
|
1387
|
-
lines.push(state.proxyDaemonCursor === ROW_DAEMON_RESTART ? chalk.bgRgb(20, 45, 60)(d1) : d1)
|
|
1388
|
-
|
|
1389
|
-
// 📖 Row 7: Stop (SIGTERM)
|
|
1390
|
-
const d2b = state.proxyDaemonCursor === ROW_DAEMON_STOP ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1391
|
-
const d2warn = chalk.dim(' (service may auto-restart)')
|
|
1392
|
-
const d2 = `${d2b}${chalk.bold('Stop service').padEnd(44)} ${chalk.dim('Enter → graceful shutdown (SIGTERM)')}${d2warn}`
|
|
1393
|
-
cursorLineByRow[ROW_DAEMON_STOP] = lines.length
|
|
1394
|
-
lines.push(state.proxyDaemonCursor === ROW_DAEMON_STOP ? chalk.bgRgb(45, 40, 20)(d2) : d2)
|
|
1395
|
-
|
|
1396
|
-
// 📖 Row 8: Force kill (SIGKILL) — emergency
|
|
1397
|
-
const d3b = state.proxyDaemonCursor === ROW_DAEMON_KILL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1398
|
-
const d3 = `${d3b}${chalk.bold.red('Force kill service').padEnd(44)} ${chalk.dim('Enter → SIGKILL — emergency only')}`
|
|
1399
|
-
cursorLineByRow[ROW_DAEMON_KILL] = lines.length
|
|
1400
|
-
lines.push(state.proxyDaemonCursor === ROW_DAEMON_KILL ? chalk.bgRgb(60, 20, 20)(d3) : d3)
|
|
1401
|
-
|
|
1402
|
-
// 📖 Row 9: View logs
|
|
1403
|
-
const d4b = state.proxyDaemonCursor === ROW_DAEMON_LOGS ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1404
|
-
const d4 = `${d4b}${chalk.bold('View service logs').padEnd(44)} ${chalk.dim('Enter → show last 50 log lines')}`
|
|
1405
|
-
cursorLineByRow[ROW_DAEMON_LOGS] = lines.length
|
|
1406
|
-
lines.push(state.proxyDaemonCursor === ROW_DAEMON_LOGS ? chalk.bgRgb(30, 30, 50)(d4) : d4)
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
// ────────────────────────────── INFO SECTION ───────────────────────────────
|
|
1410
|
-
lines.push('')
|
|
1411
|
-
lines.push(` ${chalk.bold('ℹ How it works')}`)
|
|
1412
|
-
lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
|
|
1413
|
-
lines.push('')
|
|
1414
|
-
lines.push(` ${chalk.dim(' 📖 The proxy starts a local HTTP server on 127.0.0.1 (localhost only).')}`)
|
|
1415
|
-
lines.push(` ${chalk.dim(' 📖 External tools connect to it as if it were OpenAI/Anthropic.')}`)
|
|
1416
|
-
lines.push(` ${chalk.dim(' 📖 The proxy rotates between your API keys across all providers.')}`)
|
|
1417
|
-
lines.push('')
|
|
1418
|
-
lines.push(` ${chalk.dim(' 📖 The background service adds persistence: install it once, and the proxy')}`)
|
|
1419
|
-
lines.push(` ${chalk.dim(' 📖 starts automatically at login and survives reboots.')}`)
|
|
1420
|
-
lines.push('')
|
|
1421
|
-
lines.push(` ${chalk.dim(' 📖 Claude Code support: FCM Proxy V2 translates Anthropic wire format')}`)
|
|
1422
|
-
lines.push(` ${chalk.dim(' 📖 (POST /v1/messages) to OpenAI format for upstream providers.')}`)
|
|
1423
|
-
lines.push('')
|
|
1424
|
-
if (process.platform === 'darwin') {
|
|
1425
|
-
lines.push(` ${chalk.dim(' 📦 macOS: launchd LaunchAgent at ~/Library/LaunchAgents/com.fcm.proxy.plist')}`)
|
|
1426
|
-
} else if (process.platform === 'linux') {
|
|
1427
|
-
lines.push(` ${chalk.dim(' 📦 Linux: systemd user service at ~/.config/systemd/user/fcm-proxy.service')}`)
|
|
1428
|
-
} else {
|
|
1429
|
-
lines.push(` ${chalk.dim(' ⚠ Windows: background service not supported — use in-process proxy (starts with TUI)')}`)
|
|
1430
|
-
}
|
|
1431
|
-
lines.push('')
|
|
1432
|
-
|
|
1433
|
-
// 📖 Clamp cursor
|
|
1434
|
-
if (state.proxyDaemonCursor > maxRow) state.proxyDaemonCursor = maxRow
|
|
1435
|
-
|
|
1436
|
-
// 📖 Scrolling and tinting
|
|
1437
|
-
const PROXY_DAEMON_BG = chalk.bgRgb(15, 25, 45)
|
|
1438
|
-
const targetLine = cursorLineByRow[state.proxyDaemonCursor] ?? 0
|
|
1439
|
-
state.proxyDaemonScrollOffset = keepOverlayTargetVisible(
|
|
1440
|
-
state.proxyDaemonScrollOffset,
|
|
1441
|
-
targetLine,
|
|
1442
|
-
lines.length,
|
|
1443
|
-
state.terminalRows
|
|
1444
|
-
)
|
|
1445
|
-
const { visible, offset } = sliceOverlayLines(lines, state.proxyDaemonScrollOffset, state.terminalRows)
|
|
1446
|
-
state.proxyDaemonScrollOffset = offset
|
|
1447
|
-
const tintedLines = tintOverlayLines(visible, PROXY_DAEMON_BG, state.terminalCols)
|
|
1448
|
-
return tintedLines.map(l => l + EL).join('\n')
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
959
|
// 📖 stopRecommendAnalysis: cleanup timers if user cancels during analysis
|
|
1452
960
|
function stopRecommendAnalysis() {
|
|
1453
961
|
if (state.recommendAnalysisTimer) { clearInterval(state.recommendAnalysisTimer); state.recommendAnalysisTimer = null }
|
|
@@ -1456,10 +964,8 @@ const updateRowIdx = providerKeys.length
|
|
|
1456
964
|
|
|
1457
965
|
return {
|
|
1458
966
|
renderSettings,
|
|
1459
|
-
renderProxyDaemon,
|
|
1460
967
|
renderInstallEndpoints,
|
|
1461
968
|
renderHelp,
|
|
1462
|
-
renderLog,
|
|
1463
969
|
renderRecommend,
|
|
1464
970
|
renderFeedback,
|
|
1465
971
|
renderChangelog,
|