free-coding-models 0.2.17 → 0.3.1
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 +71 -0
- package/README.md +118 -44
- package/bin/fcm-proxy-daemon.js +239 -0
- package/bin/free-coding-models.js +146 -37
- package/package.json +3 -2
- package/src/account-manager.js +34 -0
- package/src/anthropic-translator.js +440 -0
- package/src/cli-help.js +108 -0
- package/src/config.js +25 -1
- package/src/daemon-manager.js +527 -0
- package/src/endpoint-installer.js +45 -19
- package/src/key-handler.js +324 -148
- package/src/opencode.js +47 -44
- package/src/overlays.js +282 -207
- package/src/proxy-server.js +746 -10
- package/src/proxy-sync.js +564 -0
- package/src/proxy-topology.js +80 -0
- package/src/render-helpers.js +4 -2
- package/src/render-table.js +56 -49
- package/src/responses-translator.js +423 -0
- package/src/tool-launchers.js +343 -26
- package/src/utils.js +31 -8
package/src/key-handler.js
CHANGED
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
* @details
|
|
6
6
|
* This module encapsulates the full onKeyPress switch used by the TUI,
|
|
7
7
|
* including settings navigation, install-endpoint flow, overlays, profile management, and
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* tool launch actions. It also keeps the live key bindings aligned with the
|
|
9
|
+
* highlighted letters shown in the table headers.
|
|
10
|
+
*
|
|
11
|
+
* 📖 Key J opens the FCM Proxy V2 overlay directly (with daemon status refresh).
|
|
12
|
+
* 📖 Key I opens the unified "Feedback, bugs & requests" overlay.
|
|
13
|
+
* 📖 The proxy overlay handles sync toggle and cleanup for the current Z-selected tool,
|
|
14
|
+
* plus all daemon management actions.
|
|
10
15
|
*
|
|
11
16
|
* It also owns the "test key" model selection used by the Settings overlay.
|
|
12
17
|
* Some providers expose models in `/v1/models` that are not actually callable
|
|
@@ -26,6 +31,7 @@
|
|
|
26
31
|
*/
|
|
27
32
|
|
|
28
33
|
import { loadChangelog } from './changelog-loader.js'
|
|
34
|
+
import { resolveProxySyncToolMode } from './proxy-sync.js'
|
|
29
35
|
|
|
30
36
|
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
31
37
|
// 📖 is not guaranteed to be accepted by their chat endpoint.
|
|
@@ -181,7 +187,7 @@ export function createKeyHandler(ctx) {
|
|
|
181
187
|
ENV_VAR_NAMES,
|
|
182
188
|
ensureProxyRunning,
|
|
183
189
|
syncToOpenCode,
|
|
184
|
-
|
|
190
|
+
cleanupToolConfig,
|
|
185
191
|
restoreOpenCodeBackup,
|
|
186
192
|
checkForUpdateDetailed,
|
|
187
193
|
runUpdate,
|
|
@@ -195,7 +201,6 @@ export function createKeyHandler(ctx) {
|
|
|
195
201
|
getToolModeOrder,
|
|
196
202
|
startRecommendAnalysis,
|
|
197
203
|
stopRecommendAnalysis,
|
|
198
|
-
sendFeatureRequest,
|
|
199
204
|
sendBugReport,
|
|
200
205
|
stopUi,
|
|
201
206
|
ping,
|
|
@@ -619,81 +624,14 @@ export function createKeyHandler(ctx) {
|
|
|
619
624
|
return
|
|
620
625
|
}
|
|
621
626
|
|
|
622
|
-
// 📖
|
|
627
|
+
// 📖 Feedback overlay: intercept ALL keys while overlay is active.
|
|
623
628
|
// 📖 Enter → send to Discord, Esc → cancel, Backspace → delete char, printable → append to buffer.
|
|
624
|
-
if (state.
|
|
629
|
+
if (state.feedbackOpen) {
|
|
625
630
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
626
631
|
|
|
627
632
|
if (key.name === 'escape') {
|
|
628
|
-
// 📖 Cancel
|
|
629
|
-
state.
|
|
630
|
-
state.featureRequestBuffer = ''
|
|
631
|
-
state.featureRequestStatus = 'idle'
|
|
632
|
-
state.featureRequestError = null
|
|
633
|
-
return
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (key.name === 'return') {
|
|
637
|
-
// 📖 Send feature request to Discord webhook
|
|
638
|
-
const message = state.featureRequestBuffer.trim()
|
|
639
|
-
if (message.length > 0 && state.featureRequestStatus !== 'sending') {
|
|
640
|
-
state.featureRequestStatus = 'sending'
|
|
641
|
-
const result = await sendFeatureRequest(message)
|
|
642
|
-
if (result.success) {
|
|
643
|
-
// 📖 Success — show confirmation briefly, then close overlay after 3 seconds
|
|
644
|
-
state.featureRequestStatus = 'success'
|
|
645
|
-
setTimeout(() => {
|
|
646
|
-
state.featureRequestOpen = false
|
|
647
|
-
state.featureRequestBuffer = ''
|
|
648
|
-
state.featureRequestStatus = 'idle'
|
|
649
|
-
state.featureRequestError = null
|
|
650
|
-
}, 3000)
|
|
651
|
-
} else {
|
|
652
|
-
// 📖 Error — show error message, keep overlay open
|
|
653
|
-
state.featureRequestStatus = 'error'
|
|
654
|
-
state.featureRequestError = result.error || 'Unknown error'
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
if (key.name === 'backspace') {
|
|
661
|
-
// 📖 Don't allow editing while sending or after success
|
|
662
|
-
if (state.featureRequestStatus === 'sending' || state.featureRequestStatus === 'success') return
|
|
663
|
-
state.featureRequestBuffer = state.featureRequestBuffer.slice(0, -1)
|
|
664
|
-
// 📖 Clear error status when user starts editing again
|
|
665
|
-
if (state.featureRequestStatus === 'error') {
|
|
666
|
-
state.featureRequestStatus = 'idle'
|
|
667
|
-
state.featureRequestError = null
|
|
668
|
-
}
|
|
669
|
-
return
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// 📖 Append printable characters (str is the raw character typed)
|
|
673
|
-
// 📖 Limit to 500 characters (Discord embed description limit)
|
|
674
|
-
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
675
|
-
// 📖 Don't allow editing while sending or after success
|
|
676
|
-
if (state.featureRequestStatus === 'sending' || state.featureRequestStatus === 'success') return
|
|
677
|
-
if (state.featureRequestBuffer.length < 500) {
|
|
678
|
-
state.featureRequestBuffer += str
|
|
679
|
-
// 📖 Clear error status when user starts editing again
|
|
680
|
-
if (state.featureRequestStatus === 'error') {
|
|
681
|
-
state.featureRequestStatus = 'idle'
|
|
682
|
-
state.featureRequestError = null
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
return
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// 📖 Bug Report overlay: intercept ALL keys while overlay is active.
|
|
690
|
-
// 📖 Enter → send to Discord, Esc → cancel, Backspace → delete char, printable → append to buffer.
|
|
691
|
-
if (state.bugReportOpen) {
|
|
692
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
693
|
-
|
|
694
|
-
if (key.name === 'escape') {
|
|
695
|
-
// 📖 Cancel bug report — close overlay
|
|
696
|
-
state.bugReportOpen = false
|
|
633
|
+
// 📖 Cancel feedback — close overlay
|
|
634
|
+
state.feedbackOpen = false
|
|
697
635
|
state.bugReportBuffer = ''
|
|
698
636
|
state.bugReportStatus = 'idle'
|
|
699
637
|
state.bugReportError = null
|
|
@@ -701,7 +639,7 @@ export function createKeyHandler(ctx) {
|
|
|
701
639
|
}
|
|
702
640
|
|
|
703
641
|
if (key.name === 'return') {
|
|
704
|
-
// 📖 Send
|
|
642
|
+
// 📖 Send feedback to Discord webhook
|
|
705
643
|
const message = state.bugReportBuffer.trim()
|
|
706
644
|
if (message.length > 0 && state.bugReportStatus !== 'sending') {
|
|
707
645
|
state.bugReportStatus = 'sending'
|
|
@@ -710,7 +648,7 @@ export function createKeyHandler(ctx) {
|
|
|
710
648
|
// 📖 Success — show confirmation briefly, then close overlay after 3 seconds
|
|
711
649
|
state.bugReportStatus = 'success'
|
|
712
650
|
setTimeout(() => {
|
|
713
|
-
state.
|
|
651
|
+
state.feedbackOpen = false
|
|
714
652
|
state.bugReportBuffer = ''
|
|
715
653
|
state.bugReportStatus = 'idle'
|
|
716
654
|
state.bugReportError = null
|
|
@@ -1022,14 +960,11 @@ export function createKeyHandler(ctx) {
|
|
|
1022
960
|
const proxySettings = getProxySettings(state.config)
|
|
1023
961
|
const providerKeys = Object.keys(sources)
|
|
1024
962
|
const updateRowIdx = providerKeys.length
|
|
1025
|
-
const
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1028
|
-
const proxyCleanupRowIdx = updateRowIdx + 4
|
|
1029
|
-
const changelogViewRowIdx = updateRowIdx + 5
|
|
1030
|
-
// 📖 Profile rows start after maintenance + proxy rows + changelog row — one row per saved profile
|
|
963
|
+
const proxyDaemonRowIdx = updateRowIdx + 1
|
|
964
|
+
const changelogViewRowIdx = updateRowIdx + 2
|
|
965
|
+
// 📖 Profile rows start after maintenance + proxy/daemon + changelog
|
|
1031
966
|
const savedProfiles = listProfiles(state.config)
|
|
1032
|
-
const profileStartIdx = updateRowIdx +
|
|
967
|
+
const profileStartIdx = updateRowIdx + 3
|
|
1033
968
|
const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : changelogViewRowIdx
|
|
1034
969
|
|
|
1035
970
|
// 📖 Edit/Add-key mode: capture typed characters for the API key
|
|
@@ -1075,32 +1010,6 @@ export function createKeyHandler(ctx) {
|
|
|
1075
1010
|
return
|
|
1076
1011
|
}
|
|
1077
1012
|
|
|
1078
|
-
// 📖 Dedicated inline editor for the preferred proxy port. 0 = OS auto-port.
|
|
1079
|
-
if (state.settingsProxyPortEditMode) {
|
|
1080
|
-
if (key.name === 'return') {
|
|
1081
|
-
const raw = state.settingsProxyPortBuffer.trim()
|
|
1082
|
-
const parsed = raw === '' ? 0 : Number.parseInt(raw, 10)
|
|
1083
|
-
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
1084
|
-
state.settingsSyncStatus = { type: 'error', msg: '❌ Proxy port must be 0 (auto) or a number between 1 and 65535' }
|
|
1085
|
-
return
|
|
1086
|
-
}
|
|
1087
|
-
if (!state.config.settings) state.config.settings = {}
|
|
1088
|
-
state.config.settings.proxy = { ...proxySettings, preferredPort: parsed }
|
|
1089
|
-
saveConfig(state.config)
|
|
1090
|
-
state.settingsProxyPortEditMode = false
|
|
1091
|
-
state.settingsProxyPortBuffer = ''
|
|
1092
|
-
state.settingsSyncStatus = { type: 'success', msg: `✅ Preferred proxy port saved: ${parsed === 0 ? 'auto' : parsed}` }
|
|
1093
|
-
} else if (key.name === 'escape') {
|
|
1094
|
-
state.settingsProxyPortEditMode = false
|
|
1095
|
-
state.settingsProxyPortBuffer = ''
|
|
1096
|
-
} else if (key.name === 'backspace') {
|
|
1097
|
-
state.settingsProxyPortBuffer = state.settingsProxyPortBuffer.slice(0, -1)
|
|
1098
|
-
} else if (str && /^[0-9]$/.test(str) && state.settingsProxyPortBuffer.length < 5) {
|
|
1099
|
-
state.settingsProxyPortBuffer += str
|
|
1100
|
-
}
|
|
1101
|
-
return
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
1013
|
// 📖 Normal settings navigation
|
|
1105
1014
|
if (key.name === 'escape' || key.name === 'p') {
|
|
1106
1015
|
// 📖 Close settings — rebuild results to reflect provider changes
|
|
@@ -1189,18 +1098,20 @@ export function createKeyHandler(ctx) {
|
|
|
1189
1098
|
return
|
|
1190
1099
|
}
|
|
1191
1100
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
state.
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1101
|
+
// 📖 Proxy & Daemon row: Enter → open dedicated overlay
|
|
1102
|
+
if (state.settingsCursor === proxyDaemonRowIdx) {
|
|
1103
|
+
state.settingsOpen = false
|
|
1104
|
+
state.proxyDaemonOpen = true
|
|
1105
|
+
state.proxyDaemonCursor = 0
|
|
1106
|
+
state.proxyDaemonScrollOffset = 0
|
|
1107
|
+
state.proxyDaemonMessage = null
|
|
1108
|
+
// 📖 Refresh daemon status when entering
|
|
1109
|
+
try {
|
|
1110
|
+
const { getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1111
|
+
const st = await _gds()
|
|
1112
|
+
state.daemonStatus = st.status
|
|
1113
|
+
state.daemonInfo = st.info || null
|
|
1114
|
+
} catch { /* ignore */ }
|
|
1204
1115
|
return
|
|
1205
1116
|
}
|
|
1206
1117
|
|
|
@@ -1250,27 +1161,10 @@ export function createKeyHandler(ctx) {
|
|
|
1250
1161
|
}
|
|
1251
1162
|
|
|
1252
1163
|
if (key.name === 'space') {
|
|
1253
|
-
if (state.settingsCursor === updateRowIdx || state.settingsCursor ===
|
|
1164
|
+
if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyDaemonRowIdx || state.settingsCursor === changelogViewRowIdx) return
|
|
1254
1165
|
// 📖 Profile rows don't respond to Space
|
|
1255
1166
|
if (state.settingsCursor >= profileStartIdx) return
|
|
1256
1167
|
|
|
1257
|
-
if (state.settingsCursor === proxyEnabledRowIdx || state.settingsCursor === proxySyncRowIdx) {
|
|
1258
|
-
if (!state.config.settings) state.config.settings = {}
|
|
1259
|
-
state.config.settings.proxy = {
|
|
1260
|
-
...proxySettings,
|
|
1261
|
-
enabled: state.settingsCursor === proxyEnabledRowIdx ? !proxySettings.enabled : proxySettings.enabled,
|
|
1262
|
-
syncToOpenCode: state.settingsCursor === proxySyncRowIdx ? !proxySettings.syncToOpenCode : proxySettings.syncToOpenCode,
|
|
1263
|
-
}
|
|
1264
|
-
saveConfig(state.config)
|
|
1265
|
-
state.settingsSyncStatus = {
|
|
1266
|
-
type: 'success',
|
|
1267
|
-
msg: state.settingsCursor === proxyEnabledRowIdx
|
|
1268
|
-
? `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`
|
|
1269
|
-
: `✅ OpenCode proxy sync ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`,
|
|
1270
|
-
}
|
|
1271
|
-
return
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
1168
|
// 📖 Toggle enabled/disabled for selected provider
|
|
1275
1169
|
const pk = providerKeys[state.settingsCursor]
|
|
1276
1170
|
if (!state.config.providers) state.config.providers = {}
|
|
@@ -1281,7 +1175,7 @@ export function createKeyHandler(ctx) {
|
|
|
1281
1175
|
}
|
|
1282
1176
|
|
|
1283
1177
|
if (key.name === 't') {
|
|
1284
|
-
if (state.settingsCursor === updateRowIdx || state.settingsCursor ===
|
|
1178
|
+
if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyDaemonRowIdx || state.settingsCursor === changelogViewRowIdx) return
|
|
1285
1179
|
// 📖 Profile rows don't respond to T (test key)
|
|
1286
1180
|
if (state.settingsCursor >= profileStartIdx) return
|
|
1287
1181
|
|
|
@@ -1387,6 +1281,274 @@ export function createKeyHandler(ctx) {
|
|
|
1387
1281
|
return // 📖 Swallow all other keys while settings is open
|
|
1388
1282
|
}
|
|
1389
1283
|
|
|
1284
|
+
// ─── Proxy & Daemon overlay keyboard handling ─────────────────────────────
|
|
1285
|
+
if (state.proxyDaemonOpen) {
|
|
1286
|
+
const proxySettings = getProxySettings(state.config)
|
|
1287
|
+
const ROW_PROXY_ENABLED = 0
|
|
1288
|
+
const ROW_PROXY_SYNC = 1
|
|
1289
|
+
const ROW_PROXY_PORT = 2
|
|
1290
|
+
const ROW_PROXY_CLEANUP = 3
|
|
1291
|
+
const ROW_DAEMON_INSTALL = 4
|
|
1292
|
+
const ROW_DAEMON_RESTART = 5
|
|
1293
|
+
const ROW_DAEMON_STOP = 6
|
|
1294
|
+
const ROW_DAEMON_KILL = 7
|
|
1295
|
+
const ROW_DAEMON_LOGS = 8
|
|
1296
|
+
|
|
1297
|
+
const daemonStatus = state.daemonStatus || 'not-installed'
|
|
1298
|
+
const daemonIsInstalled = daemonStatus === 'running' || daemonStatus === 'stopped' || daemonStatus === 'unhealthy' || daemonStatus === 'stale'
|
|
1299
|
+
const maxRow = daemonIsInstalled ? ROW_DAEMON_LOGS : ROW_DAEMON_INSTALL
|
|
1300
|
+
|
|
1301
|
+
// 📖 Port edit mode (same as old Settings behavior)
|
|
1302
|
+
if (state.settingsProxyPortEditMode) {
|
|
1303
|
+
if (key.name === 'return') {
|
|
1304
|
+
const parsed = parseInt(state.settingsProxyPortBuffer, 10)
|
|
1305
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 65535) {
|
|
1306
|
+
state.proxyDaemonMessage = { type: 'error', msg: '❌ Port must be 0 (auto) or 1–65535', ts: Date.now() }
|
|
1307
|
+
return
|
|
1308
|
+
}
|
|
1309
|
+
if (!state.config.settings) state.config.settings = {}
|
|
1310
|
+
state.config.settings.proxy = { ...proxySettings, preferredPort: parsed }
|
|
1311
|
+
saveConfig(state.config)
|
|
1312
|
+
state.settingsProxyPortEditMode = false
|
|
1313
|
+
state.settingsProxyPortBuffer = ''
|
|
1314
|
+
state.proxyDaemonMessage = { type: 'success', msg: `✅ Preferred port saved: ${parsed === 0 ? 'auto' : parsed}`, ts: Date.now() }
|
|
1315
|
+
} else if (key.name === 'escape') {
|
|
1316
|
+
state.settingsProxyPortEditMode = false
|
|
1317
|
+
state.settingsProxyPortBuffer = ''
|
|
1318
|
+
} else if (key.name === 'backspace') {
|
|
1319
|
+
state.settingsProxyPortBuffer = state.settingsProxyPortBuffer.slice(0, -1)
|
|
1320
|
+
} else if (str && /^[0-9]$/.test(str) && state.settingsProxyPortBuffer.length < 5) {
|
|
1321
|
+
state.settingsProxyPortBuffer += str
|
|
1322
|
+
}
|
|
1323
|
+
return
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// 📖 Escape → back to Settings
|
|
1327
|
+
if (key.name === 'escape') {
|
|
1328
|
+
state.proxyDaemonOpen = false
|
|
1329
|
+
state.settingsOpen = true
|
|
1330
|
+
state.settingsProxyPortEditMode = false
|
|
1331
|
+
state.settingsProxyPortBuffer = ''
|
|
1332
|
+
return
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// 📖 Navigation
|
|
1336
|
+
if (key.name === 'up' && state.proxyDaemonCursor > 0) { state.proxyDaemonCursor--; return }
|
|
1337
|
+
if (key.name === 'down' && state.proxyDaemonCursor < maxRow) { state.proxyDaemonCursor++; return }
|
|
1338
|
+
if (key.name === 'home') { state.proxyDaemonCursor = 0; return }
|
|
1339
|
+
if (key.name === 'end') { state.proxyDaemonCursor = maxRow; return }
|
|
1340
|
+
if (key.name === 'pageup') { state.proxyDaemonCursor = Math.max(0, state.proxyDaemonCursor - 5); return }
|
|
1341
|
+
if (key.name === 'pagedown') { state.proxyDaemonCursor = Math.min(maxRow, state.proxyDaemonCursor + 5); return }
|
|
1342
|
+
|
|
1343
|
+
// 📖 Proxy sync now follows the current Z-selected tool automatically.
|
|
1344
|
+
const currentToolMode = state.mode || 'opencode'
|
|
1345
|
+
const currentProxyTool = resolveProxySyncToolMode(currentToolMode)
|
|
1346
|
+
|
|
1347
|
+
// 📖 Space toggles on proxy rows
|
|
1348
|
+
if (key.name === 'space') {
|
|
1349
|
+
if (state.proxyDaemonCursor === ROW_PROXY_ENABLED) {
|
|
1350
|
+
if (!state.config.settings) state.config.settings = {}
|
|
1351
|
+
state.config.settings.proxy = { ...proxySettings, enabled: !proxySettings.enabled }
|
|
1352
|
+
saveConfig(state.config)
|
|
1353
|
+
state.proxyDaemonMessage = { type: 'success', msg: `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`, ts: Date.now() }
|
|
1354
|
+
return
|
|
1355
|
+
}
|
|
1356
|
+
if (state.proxyDaemonCursor === ROW_PROXY_SYNC) {
|
|
1357
|
+
if (!currentProxyTool) {
|
|
1358
|
+
state.proxyDaemonMessage = { type: 'warning', msg: '⚠ Current tool does not support persisted proxy sync', ts: Date.now() }
|
|
1359
|
+
return
|
|
1360
|
+
}
|
|
1361
|
+
if (!state.config.settings) state.config.settings = {}
|
|
1362
|
+
state.config.settings.proxy = { ...proxySettings, syncToOpenCode: !proxySettings.syncToOpenCode }
|
|
1363
|
+
saveConfig(state.config)
|
|
1364
|
+
const { getToolMeta } = await import('./tool-metadata.js')
|
|
1365
|
+
const toolLabel = getToolMeta(currentProxyTool).label
|
|
1366
|
+
state.proxyDaemonMessage = { type: 'success', msg: `✅ Auto-sync to ${toolLabel} ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`, ts: Date.now() }
|
|
1367
|
+
return
|
|
1368
|
+
} else {
|
|
1369
|
+
return
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// 📖 Enter on proxy rows
|
|
1374
|
+
if (key.name === 'return') {
|
|
1375
|
+
// 📖 Proxy enabled — toggle
|
|
1376
|
+
if (state.proxyDaemonCursor === ROW_PROXY_ENABLED) {
|
|
1377
|
+
if (!state.config.settings) state.config.settings = {}
|
|
1378
|
+
state.config.settings.proxy = { ...proxySettings, enabled: !proxySettings.enabled }
|
|
1379
|
+
saveConfig(state.config)
|
|
1380
|
+
state.proxyDaemonMessage = { type: 'success', msg: `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`, ts: Date.now() }
|
|
1381
|
+
return
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// 📖 Auto-sync toggle
|
|
1385
|
+
if (state.proxyDaemonCursor === ROW_PROXY_SYNC) {
|
|
1386
|
+
if (!currentProxyTool) {
|
|
1387
|
+
state.proxyDaemonMessage = { type: 'warning', msg: '⚠ Current tool does not support persisted proxy sync', ts: Date.now() }
|
|
1388
|
+
return
|
|
1389
|
+
}
|
|
1390
|
+
if (!state.config.settings) state.config.settings = {}
|
|
1391
|
+
state.config.settings.proxy = { ...proxySettings, syncToOpenCode: !proxySettings.syncToOpenCode }
|
|
1392
|
+
saveConfig(state.config)
|
|
1393
|
+
const { getToolMeta } = await import('./tool-metadata.js')
|
|
1394
|
+
const toolLabel = getToolMeta(currentProxyTool).label
|
|
1395
|
+
state.proxyDaemonMessage = { type: 'success', msg: `✅ Auto-sync to ${toolLabel} ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`, ts: Date.now() }
|
|
1396
|
+
return
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// 📖 Port — enter edit mode
|
|
1400
|
+
if (state.proxyDaemonCursor === ROW_PROXY_PORT) {
|
|
1401
|
+
state.settingsProxyPortEditMode = true
|
|
1402
|
+
state.settingsProxyPortBuffer = String(proxySettings.preferredPort || 0)
|
|
1403
|
+
return
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// 📖 Clean proxy config — generalized for active tool
|
|
1407
|
+
if (state.proxyDaemonCursor === ROW_PROXY_CLEANUP) {
|
|
1408
|
+
if (!currentProxyTool) {
|
|
1409
|
+
state.proxyDaemonMessage = { type: 'warning', msg: '⚠ Current tool has no persisted proxy config to clean', ts: Date.now() }
|
|
1410
|
+
return
|
|
1411
|
+
}
|
|
1412
|
+
const result = cleanupToolConfig(currentProxyTool)
|
|
1413
|
+
const { getToolMeta } = await import('./tool-metadata.js')
|
|
1414
|
+
const toolLabel = getToolMeta(currentProxyTool).label
|
|
1415
|
+
if (result.success) {
|
|
1416
|
+
state.proxyDaemonMessage = { type: 'success', msg: `✅ ${toolLabel} proxy config cleaned — all fcm-* entries removed`, ts: Date.now() }
|
|
1417
|
+
} else {
|
|
1418
|
+
state.proxyDaemonMessage = { type: 'error', msg: `❌ Cleanup failed: ${result.error}`, ts: Date.now() }
|
|
1419
|
+
}
|
|
1420
|
+
return
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// 📖 Install / Uninstall daemon
|
|
1424
|
+
if (state.proxyDaemonCursor === ROW_DAEMON_INSTALL) {
|
|
1425
|
+
const { getDaemonStatus: _gds, installDaemon, uninstallDaemon, getPlatformSupport } = await import('./daemon-manager.js')
|
|
1426
|
+
const platform = getPlatformSupport()
|
|
1427
|
+
if (!platform.supported) {
|
|
1428
|
+
state.proxyDaemonMessage = { type: 'warning', msg: `⚠ ${platform.reason}`, ts: Date.now() }
|
|
1429
|
+
return
|
|
1430
|
+
}
|
|
1431
|
+
const current = await _gds()
|
|
1432
|
+
if (current.status === 'not-installed') {
|
|
1433
|
+
// 📖 Install daemon
|
|
1434
|
+
if (!proxySettings.enabled) {
|
|
1435
|
+
state.config.settings.proxy.enabled = true
|
|
1436
|
+
}
|
|
1437
|
+
state.config.settings.proxy.daemonEnabled = true
|
|
1438
|
+
state.config.settings.proxy.daemonConsent = new Date().toISOString()
|
|
1439
|
+
if (!state.config.settings.proxy.preferredPort || state.config.settings.proxy.preferredPort === 0) {
|
|
1440
|
+
state.config.settings.proxy.preferredPort = 18045
|
|
1441
|
+
}
|
|
1442
|
+
saveConfig(state.config)
|
|
1443
|
+
const result = installDaemon()
|
|
1444
|
+
if (result.success) {
|
|
1445
|
+
state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 background service installed and started!', ts: Date.now() }
|
|
1446
|
+
const ns = await _gds()
|
|
1447
|
+
state.daemonStatus = ns.status
|
|
1448
|
+
state.daemonInfo = ns.info || null
|
|
1449
|
+
} else {
|
|
1450
|
+
state.proxyDaemonMessage = { type: 'error', msg: `❌ Install failed: ${result.error}`, ts: Date.now() }
|
|
1451
|
+
}
|
|
1452
|
+
} else {
|
|
1453
|
+
// 📖 Uninstall daemon
|
|
1454
|
+
const result = uninstallDaemon()
|
|
1455
|
+
state.config.settings.proxy.daemonEnabled = false
|
|
1456
|
+
saveConfig(state.config)
|
|
1457
|
+
if (result.success) {
|
|
1458
|
+
state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 background service uninstalled.', ts: Date.now() }
|
|
1459
|
+
state.daemonStatus = 'not-installed'
|
|
1460
|
+
state.daemonInfo = null
|
|
1461
|
+
} else {
|
|
1462
|
+
state.proxyDaemonMessage = { type: 'error', msg: `❌ Uninstall failed: ${result.error}`, ts: Date.now() }
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// 📖 Restart daemon
|
|
1469
|
+
if (state.proxyDaemonCursor === ROW_DAEMON_RESTART) {
|
|
1470
|
+
const { restartDaemon, getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1471
|
+
const result = restartDaemon()
|
|
1472
|
+
if (result.success) {
|
|
1473
|
+
state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 service restarted.', ts: Date.now() }
|
|
1474
|
+
// 📖 Wait a bit for the daemon to start up
|
|
1475
|
+
setTimeout(async () => {
|
|
1476
|
+
try {
|
|
1477
|
+
const ns = await _gds()
|
|
1478
|
+
state.daemonStatus = ns.status
|
|
1479
|
+
state.daemonInfo = ns.info || null
|
|
1480
|
+
} catch { /* ignore */ }
|
|
1481
|
+
}, 2000)
|
|
1482
|
+
} else {
|
|
1483
|
+
state.proxyDaemonMessage = { type: 'error', msg: `❌ Restart failed: ${result.error}`, ts: Date.now() }
|
|
1484
|
+
}
|
|
1485
|
+
return
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// 📖 Stop daemon (SIGTERM)
|
|
1489
|
+
if (state.proxyDaemonCursor === ROW_DAEMON_STOP) {
|
|
1490
|
+
const { stopDaemon, getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1491
|
+
const result = stopDaemon()
|
|
1492
|
+
if (result.success) {
|
|
1493
|
+
const warning = result.willRestart ? ' (service may auto-restart it)' : ''
|
|
1494
|
+
state.proxyDaemonMessage = { type: 'success', msg: `✅ FCM Proxy V2 service stopped.${warning}`, ts: Date.now() }
|
|
1495
|
+
setTimeout(async () => {
|
|
1496
|
+
try {
|
|
1497
|
+
const ns = await _gds()
|
|
1498
|
+
state.daemonStatus = ns.status
|
|
1499
|
+
state.daemonInfo = ns.info || null
|
|
1500
|
+
} catch { /* ignore */ }
|
|
1501
|
+
}, 1500)
|
|
1502
|
+
} else {
|
|
1503
|
+
state.proxyDaemonMessage = { type: 'error', msg: `❌ Stop failed: ${result.error}`, ts: Date.now() }
|
|
1504
|
+
}
|
|
1505
|
+
return
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// 📖 Force kill daemon (SIGKILL) — emergency
|
|
1509
|
+
if (state.proxyDaemonCursor === ROW_DAEMON_KILL) {
|
|
1510
|
+
const { killDaemonProcess, getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1511
|
+
const result = killDaemonProcess()
|
|
1512
|
+
if (result.success) {
|
|
1513
|
+
state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 service force-killed (SIGKILL).', ts: Date.now() }
|
|
1514
|
+
const ns = await _gds()
|
|
1515
|
+
state.daemonStatus = ns.status
|
|
1516
|
+
state.daemonInfo = ns.info || null
|
|
1517
|
+
} else {
|
|
1518
|
+
state.proxyDaemonMessage = { type: 'error', msg: `❌ Kill failed: ${result.error}`, ts: Date.now() }
|
|
1519
|
+
}
|
|
1520
|
+
return
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// 📖 View daemon logs
|
|
1524
|
+
if (state.proxyDaemonCursor === ROW_DAEMON_LOGS) {
|
|
1525
|
+
const { getDaemonLogPath } = await import('./daemon-manager.js')
|
|
1526
|
+
const logPath = getDaemonLogPath()
|
|
1527
|
+
try {
|
|
1528
|
+
const { readFileSync, existsSync } = await import('node:fs')
|
|
1529
|
+
if (!existsSync(logPath)) {
|
|
1530
|
+
state.proxyDaemonMessage = { type: 'warning', msg: `⚠ No log file found at ${logPath}`, ts: Date.now() }
|
|
1531
|
+
return
|
|
1532
|
+
}
|
|
1533
|
+
const content = readFileSync(logPath, 'utf8')
|
|
1534
|
+
const logLines = content.split('\n')
|
|
1535
|
+
const last50 = logLines.slice(-50).join('\n')
|
|
1536
|
+
// 📖 Display in the log overlay (repurpose log view)
|
|
1537
|
+
state.proxyDaemonOpen = false
|
|
1538
|
+
state.logVisible = true
|
|
1539
|
+
state.logScrollOffset = 0
|
|
1540
|
+
state._daemonLogContent = last50
|
|
1541
|
+
state.proxyDaemonMessage = { type: 'success', msg: `📖 Showing last ${Math.min(50, logLines.length)} lines from ${logPath}`, ts: Date.now() }
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
state.proxyDaemonMessage = { type: 'error', msg: `❌ Could not read logs: ${err.message}`, ts: Date.now() }
|
|
1544
|
+
}
|
|
1545
|
+
return
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
return // 📖 Swallow all other keys while proxy/daemon overlay is open
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1390
1552
|
// 📖 P key: open settings screen
|
|
1391
1553
|
if (key.name === 'p' && !key.shift) {
|
|
1392
1554
|
state.settingsOpen = true
|
|
@@ -1397,6 +1559,13 @@ export function createKeyHandler(ctx) {
|
|
|
1397
1559
|
state.settingsProxyPortBuffer = ''
|
|
1398
1560
|
state.settingsEditBuffer = ''
|
|
1399
1561
|
state.settingsScrollOffset = 0
|
|
1562
|
+
// 📖 Refresh daemon status when opening settings
|
|
1563
|
+
import('./daemon-manager.js').then(dm => {
|
|
1564
|
+
dm.getDaemonStatus().then(s => {
|
|
1565
|
+
state.daemonStatus = s.status
|
|
1566
|
+
state.daemonInfo = s.info || null
|
|
1567
|
+
}).catch(() => {})
|
|
1568
|
+
}).catch(() => {})
|
|
1400
1569
|
return
|
|
1401
1570
|
}
|
|
1402
1571
|
|
|
@@ -1593,18 +1762,25 @@ export function createKeyHandler(ctx) {
|
|
|
1593
1762
|
return
|
|
1594
1763
|
}
|
|
1595
1764
|
|
|
1596
|
-
// 📖 J key: open
|
|
1765
|
+
// 📖 J key: open FCM Proxy V2 settings overlay directly (bypasses Settings screen)
|
|
1597
1766
|
if (key.name === 'j') {
|
|
1598
|
-
state.
|
|
1599
|
-
state.
|
|
1600
|
-
state.
|
|
1601
|
-
state.
|
|
1767
|
+
state.proxyDaemonOpen = true
|
|
1768
|
+
state.proxyDaemonCursor = 0
|
|
1769
|
+
state.proxyDaemonScrollOffset = 0
|
|
1770
|
+
state.proxyDaemonMessage = null
|
|
1771
|
+
// 📖 Refresh daemon status when entering
|
|
1772
|
+
try {
|
|
1773
|
+
const { getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1774
|
+
const st = await _gds()
|
|
1775
|
+
state.daemonStatus = st.status
|
|
1776
|
+
state.daemonInfo = st.info || null
|
|
1777
|
+
} catch { /* ignore */ }
|
|
1602
1778
|
return
|
|
1603
1779
|
}
|
|
1604
1780
|
|
|
1605
|
-
// 📖 I key: open
|
|
1781
|
+
// 📖 I key: open Feedback overlay (anonymous Discord feedback)
|
|
1606
1782
|
if (key.name === 'i') {
|
|
1607
|
-
state.
|
|
1783
|
+
state.feedbackOpen = true
|
|
1608
1784
|
state.bugReportBuffer = ''
|
|
1609
1785
|
state.bugReportStatus = 'idle'
|
|
1610
1786
|
state.bugReportError = null
|