free-coding-models 0.2.17 → 0.3.0
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 +53 -0
- package/README.md +96 -32
- package/bin/fcm-proxy-daemon.js +239 -0
- package/bin/free-coding-models.js +98 -20
- package/package.json +3 -2
- package/src/account-manager.js +34 -0
- package/src/anthropic-translator.js +370 -0
- package/src/config.js +24 -1
- package/src/daemon-manager.js +527 -0
- package/src/endpoint-installer.js +41 -16
- package/src/key-handler.js +327 -148
- package/src/opencode.js +30 -32
- package/src/overlays.js +272 -184
- package/src/proxy-server.js +488 -6
- package/src/proxy-sync.js +552 -0
- package/src/proxy-topology.js +80 -0
- package/src/render-table.js +24 -15
- package/src/tool-launchers.js +138 -18
package/src/overlays.js
CHANGED
|
@@ -4,20 +4,28 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @details
|
|
6
6
|
* This module centralizes all overlay rendering in one place:
|
|
7
|
-
* - Settings, Install Endpoints, Help, Log, Smart Recommend,
|
|
7
|
+
* - Settings, Install Endpoints, Help, Log, Smart Recommend, Feedback, Changelog
|
|
8
|
+
* - FCM Proxy V2 overlay with tool selector, auto-sync toggle, and cleanup
|
|
8
9
|
* - Settings diagnostics for provider key tests, including wrapped retry/error details
|
|
9
10
|
* - Recommend analysis timer orchestration and progress updates
|
|
10
11
|
*
|
|
11
12
|
* The factory pattern keeps stateful UI logic isolated while still
|
|
12
13
|
* allowing the main CLI to control shared state and dependencies.
|
|
13
14
|
*
|
|
15
|
+
* 📖 The proxy overlay rows are: Enable → Active tool → Auto-sync → Port → Cleanup → Install/Restart/Stop/Kill/Logs
|
|
16
|
+
* 📖 Tool selector cycles through PROXY_SYNCABLE_TOOLS (12 tools from proxy-sync.js)
|
|
17
|
+
* 📖 Feedback overlay (I key) combines feature requests + bug reports in one left-aligned input
|
|
18
|
+
*
|
|
14
19
|
* → Functions:
|
|
15
20
|
* - `createOverlayRenderers` — returns renderer + analysis helpers
|
|
16
21
|
*
|
|
17
22
|
* @exports { createOverlayRenderers }
|
|
23
|
+
* @see ./proxy-sync.js — PROXY_SYNCABLE_TOOLS used by the tool selector
|
|
24
|
+
* @see ./key-handler.js — handles keypresses for all overlay interactions
|
|
18
25
|
*/
|
|
19
26
|
|
|
20
27
|
import { loadChangelog } from './changelog-loader.js'
|
|
28
|
+
import { PROXY_SYNCABLE_TOOLS } from './proxy-sync.js'
|
|
21
29
|
|
|
22
30
|
export function createOverlayRenderers(state, deps) {
|
|
23
31
|
const {
|
|
@@ -138,11 +146,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
138
146
|
function renderSettings() {
|
|
139
147
|
const providerKeys = Object.keys(sources)
|
|
140
148
|
const updateRowIdx = providerKeys.length
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const proxyPortRowIdx = updateRowIdx + 3
|
|
144
|
-
const proxyCleanupRowIdx = updateRowIdx + 4
|
|
145
|
-
const changelogViewRowIdx = updateRowIdx + 5
|
|
149
|
+
const proxyDaemonRowIdx = updateRowIdx + 1
|
|
150
|
+
const changelogViewRowIdx = updateRowIdx + 2
|
|
146
151
|
const proxySettings = getProxySettings(state.config)
|
|
147
152
|
const EL = '\x1b[K'
|
|
148
153
|
const lines = []
|
|
@@ -273,33 +278,23 @@ export function createOverlayRenderers(state, deps) {
|
|
|
273
278
|
lines.push(chalk.red(` ${state.settingsUpdateError}`))
|
|
274
279
|
}
|
|
275
280
|
|
|
281
|
+
// 📖 FCM Proxy V2 — single row that opens a dedicated overlay
|
|
276
282
|
lines.push('')
|
|
277
|
-
lines.push(` ${chalk.bold('
|
|
283
|
+
lines.push(` ${chalk.bold('📡 FCM Proxy V2')}`)
|
|
278
284
|
lines.push(` ${chalk.dim(' ' + '─'.repeat(separatorWidth))}`)
|
|
279
285
|
lines.push('')
|
|
280
286
|
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const proxyPortValue = state.settingsProxyPortEditMode && state.settingsCursor === proxyPortRowIdx
|
|
293
|
-
? chalk.cyanBright(`${state.settingsProxyPortBuffer}▏`)
|
|
294
|
-
: (proxySettings.preferredPort === 0 ? chalk.dim('auto (OS-assigned)') : chalk.green(String(proxySettings.preferredPort)))
|
|
295
|
-
const proxyPortRow = `${proxyPortBullet}${chalk.bold('Preferred proxy port').padEnd(44)} ${proxyPortValue}`
|
|
296
|
-
cursorLineByRow[proxyPortRowIdx] = lines.length
|
|
297
|
-
lines.push(state.settingsCursor === proxyPortRowIdx ? chalk.bgRgb(20, 45, 60)(proxyPortRow) : proxyPortRow)
|
|
298
|
-
|
|
299
|
-
const proxyCleanupBullet = state.settingsCursor === proxyCleanupRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
300
|
-
const proxyCleanupRow = `${proxyCleanupBullet}${chalk.bold('Clean OpenCode proxy config').padEnd(44)} ${chalk.dim('Enter removes fcm-proxy from opencode.json')}`
|
|
301
|
-
cursorLineByRow[proxyCleanupRowIdx] = lines.length
|
|
302
|
-
lines.push(state.settingsCursor === proxyCleanupRowIdx ? chalk.bgRgb(45, 30, 30)(proxyCleanupRow) : proxyCleanupRow)
|
|
287
|
+
const proxyDaemonBullet = state.settingsCursor === proxyDaemonRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
288
|
+
const proxyStatus = proxySettings.enabled ? chalk.greenBright('Proxy ON') : chalk.dim('Proxy OFF')
|
|
289
|
+
const daemonStatus = state.daemonStatus || 'not-installed'
|
|
290
|
+
let daemonBadge
|
|
291
|
+
if (daemonStatus === 'running') daemonBadge = chalk.greenBright('Service ON')
|
|
292
|
+
else if (daemonStatus === 'stopped') daemonBadge = chalk.yellow('Service stopped')
|
|
293
|
+
else if (daemonStatus === 'stale' || daemonStatus === 'unhealthy') daemonBadge = chalk.red('Service ' + daemonStatus)
|
|
294
|
+
else daemonBadge = chalk.dim('Service OFF')
|
|
295
|
+
const proxyDaemonRow = `${proxyDaemonBullet}${chalk.bold('FCM Proxy V2 settings →').padEnd(44)} ${proxyStatus} ${chalk.dim('•')} ${daemonBadge}`
|
|
296
|
+
cursorLineByRow[proxyDaemonRowIdx] = lines.length
|
|
297
|
+
lines.push(state.settingsCursor === proxyDaemonRowIdx ? chalk.bgRgb(20, 45, 60)(proxyDaemonRow) : proxyDaemonRow)
|
|
303
298
|
|
|
304
299
|
// 📖 Changelog viewer row
|
|
305
300
|
const changelogViewBullet = state.settingsCursor === changelogViewRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
@@ -309,7 +304,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
309
304
|
|
|
310
305
|
// 📖 Profiles section — list saved profiles with active indicator + delete support
|
|
311
306
|
const savedProfiles = listProfiles(state.config)
|
|
312
|
-
const profileStartIdx = updateRowIdx +
|
|
307
|
+
const profileStartIdx = updateRowIdx + 3
|
|
313
308
|
const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : updateRowIdx
|
|
314
309
|
|
|
315
310
|
lines.push('')
|
|
@@ -416,7 +411,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
416
411
|
: '—'
|
|
417
412
|
|
|
418
413
|
const selectedConnectionLabel = state.installEndpointsConnectionMode === 'proxy'
|
|
419
|
-
? 'FCM Proxy'
|
|
414
|
+
? 'FCM Proxy V2'
|
|
420
415
|
: state.installEndpointsConnectionMode === 'direct'
|
|
421
416
|
? 'Direct Provider'
|
|
422
417
|
: '—'
|
|
@@ -632,10 +627,10 @@ export function createOverlayRenderers(state, deps) {
|
|
|
632
627
|
lines.push(` ${chalk.yellow('X')} Toggle token log page ${chalk.dim('(shows recent request usage from request-log.jsonl)')}`)
|
|
633
628
|
lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Claude Code → Codex → Gemini → Qwen → OpenHands → Amp)')}`)
|
|
634
629
|
lines.push(` ${chalk.yellow('F')} Toggle favorite on selected row ${chalk.dim('(⭐ pinned at top, persisted)')}`)
|
|
635
|
-
lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog → all tools, Direct or FCM Proxy)')}`)
|
|
630
|
+
lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog → all tools, Direct or FCM Proxy V2)')}`)
|
|
636
631
|
lines.push(` ${chalk.yellow('Q')} Smart Recommend ${chalk.dim('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
|
|
637
|
-
lines.push(` ${chalk.rgb(
|
|
638
|
-
lines.push(` ${chalk.
|
|
632
|
+
lines.push(` ${chalk.rgb(255, 87, 51).bold('I')} Feedback, bugs & requests ${chalk.dim('(📝 send anonymous feedback, bug reports, or feature requests)')}`)
|
|
633
|
+
lines.push(` ${chalk.yellow('J')} FCM Proxy V2 settings ${chalk.dim('(📡 open proxy configuration and background service management)')}`)
|
|
639
634
|
lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, proxy, manual update)')}`)
|
|
640
635
|
lines.push(` ${chalk.yellow('Shift+P')} Cycle config profile ${chalk.dim('(switch between saved profiles live)')}`)
|
|
641
636
|
lines.push(` ${chalk.yellow('Shift+S')} Save current config as a named profile ${chalk.dim('(inline prompt — type name + Enter)')}`)
|
|
@@ -1057,117 +1052,10 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1057
1052
|
}, PING_RATE)
|
|
1058
1053
|
}
|
|
1059
1054
|
|
|
1060
|
-
// ───
|
|
1061
|
-
// 📖
|
|
1062
|
-
// 📖 Shows an input field where users can type
|
|
1063
|
-
function
|
|
1064
|
-
const EL = '\x1b[K'
|
|
1065
|
-
const lines = []
|
|
1066
|
-
|
|
1067
|
-
// 📖 Calculate available space for multi-line input (dynamic based on terminal width)
|
|
1068
|
-
const maxInputWidth = state.terminalCols - 8 // 8 = padding (4 spaces each side)
|
|
1069
|
-
const maxInputLines = 10 // Show up to 10 lines of input
|
|
1070
|
-
|
|
1071
|
-
// 📖 Split buffer into lines for display (with wrapping)
|
|
1072
|
-
const wrapText = (text, width) => {
|
|
1073
|
-
const words = text.split(' ')
|
|
1074
|
-
const lines = []
|
|
1075
|
-
let currentLine = ''
|
|
1076
|
-
|
|
1077
|
-
for (const word of words) {
|
|
1078
|
-
const testLine = currentLine ? currentLine + ' ' + word : word
|
|
1079
|
-
if (testLine.length <= width) {
|
|
1080
|
-
currentLine = testLine
|
|
1081
|
-
} else {
|
|
1082
|
-
if (currentLine) lines.push(currentLine)
|
|
1083
|
-
currentLine = word
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
if (currentLine) lines.push(currentLine)
|
|
1087
|
-
return lines
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
const inputLines = wrapText(state.featureRequestBuffer, maxInputWidth)
|
|
1091
|
-
const displayLines = inputLines.slice(0, maxInputLines)
|
|
1092
|
-
|
|
1093
|
-
// 📖 Branding header
|
|
1094
|
-
lines.push('')
|
|
1095
|
-
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
|
|
1096
|
-
lines.push(` ${chalk.bold.rgb(57, 255, 20)('📝 Feature Request')}`)
|
|
1097
|
-
lines.push('')
|
|
1098
|
-
lines.push(chalk.dim(' — send anonymous feedback to the project team'))
|
|
1099
|
-
lines.push('')
|
|
1100
|
-
|
|
1101
|
-
// 📖 Status messages (if any)
|
|
1102
|
-
if (state.featureRequestStatus === 'sending') {
|
|
1103
|
-
lines.push(` ${chalk.yellow('⏳ Sending...')}`)
|
|
1104
|
-
lines.push('')
|
|
1105
|
-
} else if (state.featureRequestStatus === 'success') {
|
|
1106
|
-
lines.push(` ${chalk.greenBright.bold('✅ Successfully sent!')} ${chalk.dim('Closing overlay in 3 seconds...')}`)
|
|
1107
|
-
lines.push('')
|
|
1108
|
-
lines.push(` ${chalk.dim('Thank you for your feedback! Your feature request has been sent to the project team.')}`)
|
|
1109
|
-
lines.push('')
|
|
1110
|
-
} else if (state.featureRequestStatus === 'error') {
|
|
1111
|
-
lines.push(` ${chalk.red('❌ Error:')} ${chalk.yellow(state.featureRequestError || 'Failed to send')}`)
|
|
1112
|
-
lines.push(` ${chalk.dim('Press Backspace to edit, or Esc to close')}`)
|
|
1113
|
-
lines.push('')
|
|
1114
|
-
} else {
|
|
1115
|
-
lines.push(` ${chalk.dim('Type your feature request below. Press Enter to send, Esc to cancel.')}`)
|
|
1116
|
-
lines.push(` ${chalk.dim('Your message will be sent anonymously to the project team.')}`)
|
|
1117
|
-
lines.push('')
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
// 📖 Input box with border
|
|
1121
|
-
lines.push(chalk.dim(` ┌─ ${chalk.cyan('Message')} ${chalk.dim(`(${state.featureRequestBuffer.length}/500 chars)`)} ─${'─'.repeat(maxInputWidth - 22)}┐`))
|
|
1122
|
-
|
|
1123
|
-
// 📖 Display input lines (or placeholder if empty)
|
|
1124
|
-
if (displayLines.length === 0 && state.featureRequestStatus === 'idle') {
|
|
1125
|
-
lines.push(chalk.dim(` │${' '.repeat(maxInputWidth)}│`))
|
|
1126
|
-
lines.push(chalk.dim(` │ ${chalk.white.italic('Type your message here...')}${' '.repeat(Math.max(0, maxInputWidth - 28))}│`))
|
|
1127
|
-
} else {
|
|
1128
|
-
for (const line of displayLines) {
|
|
1129
|
-
const padded = line.padEnd(maxInputWidth)
|
|
1130
|
-
lines.push(` │ ${chalk.white(padded)} │`)
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
// 📖 Fill remaining space if needed
|
|
1135
|
-
const linesToFill = Math.max(0, maxInputLines - Math.max(displayLines.length, 1))
|
|
1136
|
-
for (let i = 0; i < linesToFill; i++) {
|
|
1137
|
-
lines.push(chalk.dim(` │${' '.repeat(maxInputWidth)}│`))
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// 📖 Cursor indicator (only when not sending/success)
|
|
1141
|
-
if (state.featureRequestStatus === 'idle' || state.featureRequestStatus === 'error') {
|
|
1142
|
-
// Add cursor indicator to the last line
|
|
1143
|
-
if (lines.length > 0 && displayLines.length > 0) {
|
|
1144
|
-
const lastLineIdx = lines.findIndex(l => l.includes('│ ') && !l.includes('Message'))
|
|
1145
|
-
if (lastLineIdx >= 0 && lastLineIdx < lines.length) {
|
|
1146
|
-
// Add cursor blink
|
|
1147
|
-
const lastLine = lines[lastLineIdx]
|
|
1148
|
-
if (lastLine.includes('│')) {
|
|
1149
|
-
lines[lastLineIdx] = lastLine.replace(/\s+│$/, chalk.rgb(57, 255, 20).bold('▏') + ' │')
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
lines.push(chalk.dim(` └${'─'.repeat(maxInputWidth + 2)}┘`))
|
|
1156
|
-
|
|
1157
|
-
lines.push('')
|
|
1158
|
-
lines.push(chalk.dim(' Enter Send • Esc Cancel • Backspace Delete'))
|
|
1159
|
-
|
|
1160
|
-
// 📖 Apply overlay tint and return
|
|
1161
|
-
const FEATURE_REQUEST_OVERLAY_BG = chalk.bgRgb(0, 0, 0) // Dark blue-ish background (RGB: 26, 26, 46)
|
|
1162
|
-
const tintedLines = tintOverlayLines(lines, FEATURE_REQUEST_OVERLAY_BG, state.terminalCols)
|
|
1163
|
-
const cleared = tintedLines.map(l => l + EL)
|
|
1164
|
-
return cleared.join('\n')
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// ─── Bug Report overlay renderer ─────────────────────────────────────────
|
|
1168
|
-
// 📖 renderBugReport: Draw the overlay for anonymous Discord bug reports.
|
|
1169
|
-
// 📖 Shows an input field where users can type bug reports, then sends to Discord webhook.
|
|
1170
|
-
function renderBugReport() {
|
|
1055
|
+
// ─── Feedback overlay renderer ────────────────────────────────────────────
|
|
1056
|
+
// 📖 renderFeedback: Draw the overlay for anonymous Discord feedback.
|
|
1057
|
+
// 📖 Shows an input field where users can type feedback, bug reports, or any comments.
|
|
1058
|
+
function renderFeedback() {
|
|
1171
1059
|
const EL = '\x1b[K'
|
|
1172
1060
|
const lines = []
|
|
1173
1061
|
|
|
@@ -1200,9 +1088,9 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1200
1088
|
// 📖 Branding header
|
|
1201
1089
|
lines.push('')
|
|
1202
1090
|
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
|
|
1203
|
-
lines.push(` ${chalk.bold.rgb(
|
|
1091
|
+
lines.push(` ${chalk.bold.rgb(57, 255, 20)('📝 Feedback, bugs & requests')}`)
|
|
1204
1092
|
lines.push('')
|
|
1205
|
-
lines.push(chalk.dim(
|
|
1093
|
+
lines.push(chalk.dim(" — don't hesitate to send us feedback, bug reports, or just your feeling about the app"))
|
|
1206
1094
|
lines.push('')
|
|
1207
1095
|
|
|
1208
1096
|
// 📖 Status messages (if any)
|
|
@@ -1212,55 +1100,35 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1212
1100
|
} else if (state.bugReportStatus === 'success') {
|
|
1213
1101
|
lines.push(` ${chalk.greenBright.bold('✅ Successfully sent!')} ${chalk.dim('Closing overlay in 3 seconds...')}`)
|
|
1214
1102
|
lines.push('')
|
|
1215
|
-
lines.push(` ${chalk.dim('Thank you for your feedback!
|
|
1103
|
+
lines.push(` ${chalk.dim('Thank you for your feedback! It has been sent to the project team.')}`)
|
|
1216
1104
|
lines.push('')
|
|
1217
1105
|
} else if (state.bugReportStatus === 'error') {
|
|
1218
1106
|
lines.push(` ${chalk.red('❌ Error:')} ${chalk.yellow(state.bugReportError || 'Failed to send')}`)
|
|
1219
1107
|
lines.push(` ${chalk.dim('Press Backspace to edit, or Esc to close')}`)
|
|
1220
1108
|
lines.push('')
|
|
1221
1109
|
} else {
|
|
1222
|
-
lines.push(` ${chalk.dim('
|
|
1110
|
+
lines.push(` ${chalk.dim('Type your feedback below. Press Enter to send, Esc to cancel.')}`)
|
|
1223
1111
|
lines.push(` ${chalk.dim('Your message will be sent anonymously to the project team.')}`)
|
|
1224
1112
|
lines.push('')
|
|
1225
1113
|
}
|
|
1226
1114
|
|
|
1227
|
-
// 📖
|
|
1228
|
-
lines.push(
|
|
1229
|
-
|
|
1230
|
-
// 📖
|
|
1231
|
-
if (displayLines.length
|
|
1232
|
-
lines.push(chalk.dim(` │${' '.repeat(maxInputWidth)}│`))
|
|
1233
|
-
lines.push(chalk.dim(` │ ${chalk.white.italic('Describe what happened...')}${' '.repeat(Math.max(0, maxInputWidth - 31))}│`))
|
|
1234
|
-
} else {
|
|
1115
|
+
// 📖 Simple input area – left-aligned, framed by horizontal lines
|
|
1116
|
+
lines.push(` ${chalk.cyan('Message')} (${state.bugReportBuffer.length}/500 chars)`)
|
|
1117
|
+
lines.push(` ${chalk.dim('─'.repeat(maxInputWidth))}`)
|
|
1118
|
+
// 📖 Input lines — left-aligned, or placeholder when empty
|
|
1119
|
+
if (displayLines.length > 0) {
|
|
1235
1120
|
for (const line of displayLines) {
|
|
1236
|
-
|
|
1237
|
-
lines.push(` │ ${chalk.white(padded)} │`)
|
|
1121
|
+
lines.push(` ${line}`)
|
|
1238
1122
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const linesToFill = Math.max(0, maxInputLines - Math.max(displayLines.length, 1))
|
|
1243
|
-
for (let i = 0; i < linesToFill; i++) {
|
|
1244
|
-
lines.push(chalk.dim(` │${' '.repeat(maxInputWidth)}│`))
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
// 📖 Cursor indicator (only when not sending/success)
|
|
1248
|
-
if (state.bugReportStatus === 'idle' || state.bugReportStatus === 'error') {
|
|
1249
|
-
// Add cursor indicator to the last line
|
|
1250
|
-
if (lines.length > 0 && displayLines.length > 0) {
|
|
1251
|
-
const lastLineIdx = lines.findIndex(l => l.includes('│ ') && !l.includes('Bug Details'))
|
|
1252
|
-
if (lastLineIdx >= 0 && lastLineIdx < lines.length) {
|
|
1253
|
-
// Add cursor blink
|
|
1254
|
-
const lastLine = lines[lastLineIdx]
|
|
1255
|
-
if (lastLine.includes('│')) {
|
|
1256
|
-
lines[lastLineIdx] = lastLine.replace(/\s+│$/, chalk.rgb(255, 87, 51).bold('▏') + ' │')
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1123
|
+
// 📖 Show cursor on last line
|
|
1124
|
+
if (state.bugReportStatus === 'idle' || state.bugReportStatus === 'error') {
|
|
1125
|
+
lines[lines.length - 1] += chalk.cyanBright('▏')
|
|
1259
1126
|
}
|
|
1127
|
+
} else {
|
|
1128
|
+
const placeholderBR = state.bugReportStatus === 'idle' ? chalk.white.italic('Type your message here...') : ''
|
|
1129
|
+
lines.push(` ${placeholderBR}${chalk.cyanBright('▏')}`)
|
|
1260
1130
|
}
|
|
1261
|
-
|
|
1262
|
-
lines.push(chalk.dim(` └${'─'.repeat(maxInputWidth + 2)}┘`))
|
|
1263
|
-
|
|
1131
|
+
lines.push(` ${chalk.dim('─'.repeat(maxInputWidth))}`)
|
|
1264
1132
|
lines.push('')
|
|
1265
1133
|
lines.push(chalk.dim(' Enter Send • Esc Cancel • Backspace Delete'))
|
|
1266
1134
|
|
|
@@ -1393,6 +1261,226 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1393
1261
|
return cleared.join('\n')
|
|
1394
1262
|
}
|
|
1395
1263
|
|
|
1264
|
+
// ─── FCM Proxy V2 overlay renderer ──────────────────────────────────────────
|
|
1265
|
+
// 📖 renderProxyDaemon: Dedicated full-page overlay for FCM Proxy V2 configuration
|
|
1266
|
+
// 📖 and background service management. Opened from Settings → "FCM Proxy V2 settings →".
|
|
1267
|
+
// 📖 Contains all proxy toggles, service status/actions, explanations, and emergency kill.
|
|
1268
|
+
function renderProxyDaemon() {
|
|
1269
|
+
const EL = '\x1b[K'
|
|
1270
|
+
const lines = []
|
|
1271
|
+
const cursorLineByRow = {}
|
|
1272
|
+
const proxySettings = getProxySettings(state.config)
|
|
1273
|
+
|
|
1274
|
+
// 📖 Row indices — these control cursor navigation
|
|
1275
|
+
const ROW_PROXY_ENABLED = 0
|
|
1276
|
+
const ROW_PROXY_TOOL = 1
|
|
1277
|
+
const ROW_PROXY_SYNC = 2
|
|
1278
|
+
const ROW_PROXY_PORT = 3
|
|
1279
|
+
const ROW_PROXY_CLEANUP = 4
|
|
1280
|
+
const ROW_DAEMON_INSTALL = 5
|
|
1281
|
+
const ROW_DAEMON_RESTART = 6
|
|
1282
|
+
const ROW_DAEMON_STOP = 7
|
|
1283
|
+
const ROW_DAEMON_KILL = 8
|
|
1284
|
+
const ROW_DAEMON_LOGS = 9
|
|
1285
|
+
|
|
1286
|
+
const daemonStatus = state.daemonStatus || 'not-installed'
|
|
1287
|
+
const daemonInfo = state.daemonInfo
|
|
1288
|
+
const daemonIsActive = daemonStatus === 'running' || daemonStatus === 'unhealthy' || daemonStatus === 'stale'
|
|
1289
|
+
const daemonIsInstalled = daemonIsActive || daemonStatus === 'stopped'
|
|
1290
|
+
|
|
1291
|
+
// 📖 Compute max row — hide daemon action rows when daemon not installed
|
|
1292
|
+
let maxRow = ROW_DAEMON_INSTALL
|
|
1293
|
+
if (daemonIsInstalled) maxRow = ROW_DAEMON_LOGS
|
|
1294
|
+
|
|
1295
|
+
// 📖 Header
|
|
1296
|
+
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
|
|
1297
|
+
lines.push(` ${chalk.bold('📡 FCM Proxy V2 Manager')}`)
|
|
1298
|
+
lines.push(` ${chalk.dim('— Esc back to Settings • ↑↓ navigate • Enter select')}`)
|
|
1299
|
+
lines.push('')
|
|
1300
|
+
lines.push(` ${chalk.bgRed.white.bold(' ⚠ EXPERIMENTAL ')} ${chalk.red('This feature is under active development and may not work as expected.')}`)
|
|
1301
|
+
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.')}`)
|
|
1302
|
+
lines.push('')
|
|
1303
|
+
|
|
1304
|
+
// 📖 Feedback message (auto-clears after 5s)
|
|
1305
|
+
const msg = state.proxyDaemonMessage
|
|
1306
|
+
if (msg && (Date.now() - msg.ts < 5000)) {
|
|
1307
|
+
const msgColor = msg.type === 'success' ? chalk.greenBright : msg.type === 'warning' ? chalk.yellow : chalk.red
|
|
1308
|
+
lines.push(` ${msgColor(msg.msg)}`)
|
|
1309
|
+
lines.push('')
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// ────────────────────────────── PROXY SECTION ──────────────────────────────
|
|
1313
|
+
lines.push(` ${chalk.bold('🔀 Proxy Configuration')}`)
|
|
1314
|
+
lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
|
|
1315
|
+
lines.push('')
|
|
1316
|
+
lines.push(` ${chalk.dim(' The local proxy groups all your provider API keys into a single')}`)
|
|
1317
|
+
lines.push(` ${chalk.dim(' endpoint. Tools like OpenCode, Claude Code, Goose, etc. connect')}`)
|
|
1318
|
+
lines.push(` ${chalk.dim(' to this proxy which handles key rotation, rate limiting, and failover.')}`)
|
|
1319
|
+
lines.push('')
|
|
1320
|
+
|
|
1321
|
+
// 📖 Resolve active tool for proxy sync (persisted or fallback to Z-mode)
|
|
1322
|
+
const activeProxyTool = proxySettings.activeTool || state.mode || 'opencode'
|
|
1323
|
+
const activeToolMeta = getToolMeta(activeProxyTool)
|
|
1324
|
+
const activeToolLabel = `${activeToolMeta.emoji} ${activeToolMeta.label}`
|
|
1325
|
+
|
|
1326
|
+
// 📖 Row 0: Proxy enabled toggle
|
|
1327
|
+
const r0b = state.proxyDaemonCursor === ROW_PROXY_ENABLED ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1328
|
+
const r0val = proxySettings.enabled ? chalk.greenBright('Enabled') : chalk.dim('Disabled (opt-in)')
|
|
1329
|
+
const r0 = `${r0b}${chalk.bold('Proxy mode').padEnd(44)} ${r0val}`
|
|
1330
|
+
cursorLineByRow[ROW_PROXY_ENABLED] = lines.length
|
|
1331
|
+
lines.push(state.proxyDaemonCursor === ROW_PROXY_ENABLED ? chalk.bgRgb(20, 45, 60)(r0) : r0)
|
|
1332
|
+
|
|
1333
|
+
// 📖 Row 1: Active tool selector — cycles through proxy-syncable tools
|
|
1334
|
+
const r1b = state.proxyDaemonCursor === ROW_PROXY_TOOL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1335
|
+
const r1 = `${r1b}${chalk.bold('Active tool').padEnd(44)} ${chalk.cyanBright(activeToolLabel)} ${chalk.dim('← Enter to cycle')}`
|
|
1336
|
+
cursorLineByRow[ROW_PROXY_TOOL] = lines.length
|
|
1337
|
+
lines.push(state.proxyDaemonCursor === ROW_PROXY_TOOL ? chalk.bgRgb(20, 45, 60)(r1) : r1)
|
|
1338
|
+
|
|
1339
|
+
// 📖 Row 2: Auto-sync proxy config to active tool
|
|
1340
|
+
const r2b = state.proxyDaemonCursor === ROW_PROXY_SYNC ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1341
|
+
const r2val = proxySettings.syncToOpenCode ? chalk.greenBright('Enabled') : chalk.dim('Disabled')
|
|
1342
|
+
const r2 = `${r2b}${chalk.bold(`Auto-sync proxy to ${activeToolMeta.label}`).padEnd(44)} ${r2val}`
|
|
1343
|
+
cursorLineByRow[ROW_PROXY_SYNC] = lines.length
|
|
1344
|
+
lines.push(state.proxyDaemonCursor === ROW_PROXY_SYNC ? chalk.bgRgb(20, 45, 60)(r2) : r2)
|
|
1345
|
+
|
|
1346
|
+
// 📖 Row 3: Preferred port
|
|
1347
|
+
const r3b = state.proxyDaemonCursor === ROW_PROXY_PORT ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1348
|
+
const r3val = state.settingsProxyPortEditMode && state.proxyDaemonCursor === ROW_PROXY_PORT
|
|
1349
|
+
? chalk.cyanBright(`${state.settingsProxyPortBuffer}▏`)
|
|
1350
|
+
: (proxySettings.preferredPort === 0 ? chalk.dim('auto (OS-assigned)') : chalk.green(String(proxySettings.preferredPort)))
|
|
1351
|
+
const r3 = `${r3b}${chalk.bold('Preferred proxy port').padEnd(44)} ${r3val}`
|
|
1352
|
+
cursorLineByRow[ROW_PROXY_PORT] = lines.length
|
|
1353
|
+
lines.push(state.proxyDaemonCursor === ROW_PROXY_PORT ? chalk.bgRgb(20, 45, 60)(r3) : r3)
|
|
1354
|
+
|
|
1355
|
+
// 📖 Row 4: Clean tool proxy config
|
|
1356
|
+
const r4b = state.proxyDaemonCursor === ROW_PROXY_CLEANUP ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1357
|
+
const r4 = `${r4b}${chalk.bold(`Clean ${activeToolMeta.label} proxy config`).padEnd(44)} ${chalk.dim('Enter → removes all fcm-* entries')}`
|
|
1358
|
+
cursorLineByRow[ROW_PROXY_CLEANUP] = lines.length
|
|
1359
|
+
lines.push(state.proxyDaemonCursor === ROW_PROXY_CLEANUP ? chalk.bgRgb(45, 30, 30)(r4) : r4)
|
|
1360
|
+
|
|
1361
|
+
// ────────────────────────────── DAEMON SECTION ─────────────────────────────
|
|
1362
|
+
lines.push('')
|
|
1363
|
+
lines.push(` ${chalk.bold('📡 FCM Proxy V2 Background Service')}`)
|
|
1364
|
+
lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
|
|
1365
|
+
lines.push('')
|
|
1366
|
+
lines.push(` ${chalk.dim(' The background service keeps FCM Proxy V2 running 24/7 — even when')}`)
|
|
1367
|
+
lines.push(` ${chalk.dim(' the TUI is closed or after a reboot. Claude Code, Gemini CLI, and')}`)
|
|
1368
|
+
lines.push(` ${chalk.dim(' all tools stay connected at all times.')}`)
|
|
1369
|
+
lines.push('')
|
|
1370
|
+
|
|
1371
|
+
// 📖 Status display
|
|
1372
|
+
let daemonStatusLine = ` ${chalk.bold(' Status:')} `
|
|
1373
|
+
if (daemonStatus === 'running') {
|
|
1374
|
+
daemonStatusLine += chalk.greenBright('● Running')
|
|
1375
|
+
if (daemonInfo) daemonStatusLine += chalk.dim(` — PID ${daemonInfo.pid} • Port ${daemonInfo.port} • ${daemonInfo.accountCount || '?'} accounts • ${daemonInfo.modelCount || '?'} models`)
|
|
1376
|
+
} else if (daemonStatus === 'stopped') {
|
|
1377
|
+
daemonStatusLine += chalk.yellow('○ Stopped') + chalk.dim(' — service installed but not running')
|
|
1378
|
+
} else if (daemonStatus === 'stale') {
|
|
1379
|
+
daemonStatusLine += chalk.red('⚠ Stale') + chalk.dim(' — service crashed, PID no longer alive')
|
|
1380
|
+
} else if (daemonStatus === 'unhealthy') {
|
|
1381
|
+
daemonStatusLine += chalk.red('⚠ Unhealthy') + chalk.dim(' — PID alive but health check failed')
|
|
1382
|
+
} else {
|
|
1383
|
+
daemonStatusLine += chalk.dim('○ Not installed')
|
|
1384
|
+
}
|
|
1385
|
+
lines.push(daemonStatusLine)
|
|
1386
|
+
|
|
1387
|
+
// 📖 Version mismatch warning
|
|
1388
|
+
if (daemonInfo?.version && daemonInfo.version !== LOCAL_VERSION) {
|
|
1389
|
+
lines.push(` ${chalk.yellow(` ⚠ Version mismatch: service v${daemonInfo.version} vs FCM v${LOCAL_VERSION}`)}`)
|
|
1390
|
+
lines.push(` ${chalk.dim(' Restart or reinstall the service to apply the update.')}`)
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// 📖 Uptime
|
|
1394
|
+
if (daemonStatus === 'running' && daemonInfo?.startedAt) {
|
|
1395
|
+
const upSec = Math.floor((Date.now() - new Date(daemonInfo.startedAt).getTime()) / 1000)
|
|
1396
|
+
const upMin = Math.floor(upSec / 60)
|
|
1397
|
+
const upHr = Math.floor(upMin / 60)
|
|
1398
|
+
const uptimeStr = upHr > 0 ? `${upHr}h ${upMin % 60}m` : upMin > 0 ? `${upMin}m ${upSec % 60}s` : `${upSec}s`
|
|
1399
|
+
lines.push(` ${chalk.dim(` Uptime: ${uptimeStr}`)}`)
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
lines.push('')
|
|
1403
|
+
|
|
1404
|
+
// 📖 Row 5: Install / Uninstall
|
|
1405
|
+
const d0b = state.proxyDaemonCursor === ROW_DAEMON_INSTALL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1406
|
+
const d0label = daemonIsInstalled ? 'Uninstall service' : 'Install background service'
|
|
1407
|
+
const d0hint = daemonIsInstalled
|
|
1408
|
+
? chalk.dim('Enter → stop service + remove config')
|
|
1409
|
+
: chalk.dim('Enter → install as OS service (launchd/systemd)')
|
|
1410
|
+
const d0 = `${d0b}${chalk.bold(d0label).padEnd(44)} ${d0hint}`
|
|
1411
|
+
cursorLineByRow[ROW_DAEMON_INSTALL] = lines.length
|
|
1412
|
+
lines.push(state.proxyDaemonCursor === ROW_DAEMON_INSTALL ? chalk.bgRgb(daemonIsInstalled ? 45 : 20, daemonIsInstalled ? 30 : 45, daemonIsInstalled ? 30 : 40)(d0) : d0)
|
|
1413
|
+
|
|
1414
|
+
// 📖 Rows 6-9 only shown when service is installed
|
|
1415
|
+
if (daemonIsInstalled) {
|
|
1416
|
+
// 📖 Row 6: Restart
|
|
1417
|
+
const d1b = state.proxyDaemonCursor === ROW_DAEMON_RESTART ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1418
|
+
const d1 = `${d1b}${chalk.bold('Restart service').padEnd(44)} ${chalk.dim('Enter → stop + start via OS service manager')}`
|
|
1419
|
+
cursorLineByRow[ROW_DAEMON_RESTART] = lines.length
|
|
1420
|
+
lines.push(state.proxyDaemonCursor === ROW_DAEMON_RESTART ? chalk.bgRgb(20, 45, 60)(d1) : d1)
|
|
1421
|
+
|
|
1422
|
+
// 📖 Row 7: Stop (SIGTERM)
|
|
1423
|
+
const d2b = state.proxyDaemonCursor === ROW_DAEMON_STOP ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1424
|
+
const d2warn = chalk.dim(' (service may auto-restart)')
|
|
1425
|
+
const d2 = `${d2b}${chalk.bold('Stop service').padEnd(44)} ${chalk.dim('Enter → graceful shutdown (SIGTERM)')}${d2warn}`
|
|
1426
|
+
cursorLineByRow[ROW_DAEMON_STOP] = lines.length
|
|
1427
|
+
lines.push(state.proxyDaemonCursor === ROW_DAEMON_STOP ? chalk.bgRgb(45, 40, 20)(d2) : d2)
|
|
1428
|
+
|
|
1429
|
+
// 📖 Row 8: Force kill (SIGKILL) — emergency
|
|
1430
|
+
const d3b = state.proxyDaemonCursor === ROW_DAEMON_KILL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1431
|
+
const d3 = `${d3b}${chalk.bold.red('Force kill service').padEnd(44)} ${chalk.dim('Enter → SIGKILL — emergency only')}`
|
|
1432
|
+
cursorLineByRow[ROW_DAEMON_KILL] = lines.length
|
|
1433
|
+
lines.push(state.proxyDaemonCursor === ROW_DAEMON_KILL ? chalk.bgRgb(60, 20, 20)(d3) : d3)
|
|
1434
|
+
|
|
1435
|
+
// 📖 Row 9: View logs
|
|
1436
|
+
const d4b = state.proxyDaemonCursor === ROW_DAEMON_LOGS ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1437
|
+
const d4 = `${d4b}${chalk.bold('View service logs').padEnd(44)} ${chalk.dim('Enter → show last 50 log lines')}`
|
|
1438
|
+
cursorLineByRow[ROW_DAEMON_LOGS] = lines.length
|
|
1439
|
+
lines.push(state.proxyDaemonCursor === ROW_DAEMON_LOGS ? chalk.bgRgb(30, 30, 50)(d4) : d4)
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// ────────────────────────────── INFO SECTION ───────────────────────────────
|
|
1443
|
+
lines.push('')
|
|
1444
|
+
lines.push(` ${chalk.bold('ℹ How it works')}`)
|
|
1445
|
+
lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
|
|
1446
|
+
lines.push('')
|
|
1447
|
+
lines.push(` ${chalk.dim(' 📖 The proxy starts a local HTTP server on 127.0.0.1 (localhost only).')}`)
|
|
1448
|
+
lines.push(` ${chalk.dim(' 📖 External tools connect to it as if it were OpenAI/Anthropic.')}`)
|
|
1449
|
+
lines.push(` ${chalk.dim(' 📖 The proxy rotates between your API keys across all providers.')}`)
|
|
1450
|
+
lines.push('')
|
|
1451
|
+
lines.push(` ${chalk.dim(' 📖 The background service adds persistence: install it once, and the proxy')}`)
|
|
1452
|
+
lines.push(` ${chalk.dim(' 📖 starts automatically at login and survives reboots.')}`)
|
|
1453
|
+
lines.push('')
|
|
1454
|
+
lines.push(` ${chalk.dim(' 📖 Claude Code support: FCM Proxy V2 translates Anthropic wire format')}`)
|
|
1455
|
+
lines.push(` ${chalk.dim(' 📖 (POST /v1/messages) to OpenAI format for upstream providers.')}`)
|
|
1456
|
+
lines.push('')
|
|
1457
|
+
if (process.platform === 'darwin') {
|
|
1458
|
+
lines.push(` ${chalk.dim(' 📦 macOS: launchd LaunchAgent at ~/Library/LaunchAgents/com.fcm.proxy.plist')}`)
|
|
1459
|
+
} else if (process.platform === 'linux') {
|
|
1460
|
+
lines.push(` ${chalk.dim(' 📦 Linux: systemd user service at ~/.config/systemd/user/fcm-proxy.service')}`)
|
|
1461
|
+
} else {
|
|
1462
|
+
lines.push(` ${chalk.dim(' ⚠ Windows: background service not supported — use in-process proxy (starts with TUI)')}`)
|
|
1463
|
+
}
|
|
1464
|
+
lines.push('')
|
|
1465
|
+
|
|
1466
|
+
// 📖 Clamp cursor
|
|
1467
|
+
if (state.proxyDaemonCursor > maxRow) state.proxyDaemonCursor = maxRow
|
|
1468
|
+
|
|
1469
|
+
// 📖 Scrolling and tinting
|
|
1470
|
+
const PROXY_DAEMON_BG = chalk.bgRgb(15, 25, 45)
|
|
1471
|
+
const targetLine = cursorLineByRow[state.proxyDaemonCursor] ?? 0
|
|
1472
|
+
state.proxyDaemonScrollOffset = keepOverlayTargetVisible(
|
|
1473
|
+
state.proxyDaemonScrollOffset,
|
|
1474
|
+
targetLine,
|
|
1475
|
+
lines.length,
|
|
1476
|
+
state.terminalRows
|
|
1477
|
+
)
|
|
1478
|
+
const { visible, offset } = sliceOverlayLines(lines, state.proxyDaemonScrollOffset, state.terminalRows)
|
|
1479
|
+
state.proxyDaemonScrollOffset = offset
|
|
1480
|
+
const tintedLines = tintOverlayLines(visible, PROXY_DAEMON_BG, state.terminalCols)
|
|
1481
|
+
return tintedLines.map(l => l + EL).join('\n')
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1396
1484
|
// 📖 stopRecommendAnalysis: cleanup timers if user cancels during analysis
|
|
1397
1485
|
function stopRecommendAnalysis() {
|
|
1398
1486
|
if (state.recommendAnalysisTimer) { clearInterval(state.recommendAnalysisTimer); state.recommendAnalysisTimer = null }
|
|
@@ -1401,12 +1489,12 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1401
1489
|
|
|
1402
1490
|
return {
|
|
1403
1491
|
renderSettings,
|
|
1492
|
+
renderProxyDaemon,
|
|
1404
1493
|
renderInstallEndpoints,
|
|
1405
1494
|
renderHelp,
|
|
1406
1495
|
renderLog,
|
|
1407
1496
|
renderRecommend,
|
|
1408
|
-
|
|
1409
|
-
renderBugReport,
|
|
1497
|
+
renderFeedback,
|
|
1410
1498
|
renderChangelog,
|
|
1411
1499
|
startRecommendAnalysis,
|
|
1412
1500
|
stopRecommendAnalysis,
|