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/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 tool cycling (ROW_PROXY_TOOL), sync toggle, cleanup
|
|
14
|
+
* for any syncable tool via cleanupToolConfig(), and 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
|
|
@@ -181,7 +186,7 @@ export function createKeyHandler(ctx) {
|
|
|
181
186
|
ENV_VAR_NAMES,
|
|
182
187
|
ensureProxyRunning,
|
|
183
188
|
syncToOpenCode,
|
|
184
|
-
|
|
189
|
+
cleanupToolConfig,
|
|
185
190
|
restoreOpenCodeBackup,
|
|
186
191
|
checkForUpdateDetailed,
|
|
187
192
|
runUpdate,
|
|
@@ -195,7 +200,6 @@ export function createKeyHandler(ctx) {
|
|
|
195
200
|
getToolModeOrder,
|
|
196
201
|
startRecommendAnalysis,
|
|
197
202
|
stopRecommendAnalysis,
|
|
198
|
-
sendFeatureRequest,
|
|
199
203
|
sendBugReport,
|
|
200
204
|
stopUi,
|
|
201
205
|
ping,
|
|
@@ -619,81 +623,14 @@ export function createKeyHandler(ctx) {
|
|
|
619
623
|
return
|
|
620
624
|
}
|
|
621
625
|
|
|
622
|
-
// 📖
|
|
626
|
+
// 📖 Feedback overlay: intercept ALL keys while overlay is active.
|
|
623
627
|
// 📖 Enter → send to Discord, Esc → cancel, Backspace → delete char, printable → append to buffer.
|
|
624
|
-
if (state.
|
|
628
|
+
if (state.feedbackOpen) {
|
|
625
629
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
626
630
|
|
|
627
631
|
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
|
|
632
|
+
// 📖 Cancel feedback — close overlay
|
|
633
|
+
state.feedbackOpen = false
|
|
697
634
|
state.bugReportBuffer = ''
|
|
698
635
|
state.bugReportStatus = 'idle'
|
|
699
636
|
state.bugReportError = null
|
|
@@ -701,7 +638,7 @@ export function createKeyHandler(ctx) {
|
|
|
701
638
|
}
|
|
702
639
|
|
|
703
640
|
if (key.name === 'return') {
|
|
704
|
-
// 📖 Send
|
|
641
|
+
// 📖 Send feedback to Discord webhook
|
|
705
642
|
const message = state.bugReportBuffer.trim()
|
|
706
643
|
if (message.length > 0 && state.bugReportStatus !== 'sending') {
|
|
707
644
|
state.bugReportStatus = 'sending'
|
|
@@ -710,7 +647,7 @@ export function createKeyHandler(ctx) {
|
|
|
710
647
|
// 📖 Success — show confirmation briefly, then close overlay after 3 seconds
|
|
711
648
|
state.bugReportStatus = 'success'
|
|
712
649
|
setTimeout(() => {
|
|
713
|
-
state.
|
|
650
|
+
state.feedbackOpen = false
|
|
714
651
|
state.bugReportBuffer = ''
|
|
715
652
|
state.bugReportStatus = 'idle'
|
|
716
653
|
state.bugReportError = null
|
|
@@ -1022,14 +959,11 @@ export function createKeyHandler(ctx) {
|
|
|
1022
959
|
const proxySettings = getProxySettings(state.config)
|
|
1023
960
|
const providerKeys = Object.keys(sources)
|
|
1024
961
|
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
|
|
962
|
+
const proxyDaemonRowIdx = updateRowIdx + 1
|
|
963
|
+
const changelogViewRowIdx = updateRowIdx + 2
|
|
964
|
+
// 📖 Profile rows start after maintenance + proxy/daemon + changelog
|
|
1031
965
|
const savedProfiles = listProfiles(state.config)
|
|
1032
|
-
const profileStartIdx = updateRowIdx +
|
|
966
|
+
const profileStartIdx = updateRowIdx + 3
|
|
1033
967
|
const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : changelogViewRowIdx
|
|
1034
968
|
|
|
1035
969
|
// 📖 Edit/Add-key mode: capture typed characters for the API key
|
|
@@ -1075,32 +1009,6 @@ export function createKeyHandler(ctx) {
|
|
|
1075
1009
|
return
|
|
1076
1010
|
}
|
|
1077
1011
|
|
|
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
1012
|
// 📖 Normal settings navigation
|
|
1105
1013
|
if (key.name === 'escape' || key.name === 'p') {
|
|
1106
1014
|
// 📖 Close settings — rebuild results to reflect provider changes
|
|
@@ -1189,18 +1097,20 @@ export function createKeyHandler(ctx) {
|
|
|
1189
1097
|
return
|
|
1190
1098
|
}
|
|
1191
1099
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
state.
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1100
|
+
// 📖 Proxy & Daemon row: Enter → open dedicated overlay
|
|
1101
|
+
if (state.settingsCursor === proxyDaemonRowIdx) {
|
|
1102
|
+
state.settingsOpen = false
|
|
1103
|
+
state.proxyDaemonOpen = true
|
|
1104
|
+
state.proxyDaemonCursor = 0
|
|
1105
|
+
state.proxyDaemonScrollOffset = 0
|
|
1106
|
+
state.proxyDaemonMessage = null
|
|
1107
|
+
// 📖 Refresh daemon status when entering
|
|
1108
|
+
try {
|
|
1109
|
+
const { getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1110
|
+
const st = await _gds()
|
|
1111
|
+
state.daemonStatus = st.status
|
|
1112
|
+
state.daemonInfo = st.info || null
|
|
1113
|
+
} catch { /* ignore */ }
|
|
1204
1114
|
return
|
|
1205
1115
|
}
|
|
1206
1116
|
|
|
@@ -1250,27 +1160,10 @@ export function createKeyHandler(ctx) {
|
|
|
1250
1160
|
}
|
|
1251
1161
|
|
|
1252
1162
|
if (key.name === 'space') {
|
|
1253
|
-
if (state.settingsCursor === updateRowIdx || state.settingsCursor ===
|
|
1163
|
+
if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyDaemonRowIdx || state.settingsCursor === changelogViewRowIdx) return
|
|
1254
1164
|
// 📖 Profile rows don't respond to Space
|
|
1255
1165
|
if (state.settingsCursor >= profileStartIdx) return
|
|
1256
1166
|
|
|
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
1167
|
// 📖 Toggle enabled/disabled for selected provider
|
|
1275
1168
|
const pk = providerKeys[state.settingsCursor]
|
|
1276
1169
|
if (!state.config.providers) state.config.providers = {}
|
|
@@ -1281,7 +1174,7 @@ export function createKeyHandler(ctx) {
|
|
|
1281
1174
|
}
|
|
1282
1175
|
|
|
1283
1176
|
if (key.name === 't') {
|
|
1284
|
-
if (state.settingsCursor === updateRowIdx || state.settingsCursor ===
|
|
1177
|
+
if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyDaemonRowIdx || state.settingsCursor === changelogViewRowIdx) return
|
|
1285
1178
|
// 📖 Profile rows don't respond to T (test key)
|
|
1286
1179
|
if (state.settingsCursor >= profileStartIdx) return
|
|
1287
1180
|
|
|
@@ -1387,6 +1280,278 @@ export function createKeyHandler(ctx) {
|
|
|
1387
1280
|
return // 📖 Swallow all other keys while settings is open
|
|
1388
1281
|
}
|
|
1389
1282
|
|
|
1283
|
+
// ─── Proxy & Daemon overlay keyboard handling ─────────────────────────────
|
|
1284
|
+
if (state.proxyDaemonOpen) {
|
|
1285
|
+
const proxySettings = getProxySettings(state.config)
|
|
1286
|
+
const ROW_PROXY_ENABLED = 0
|
|
1287
|
+
const ROW_PROXY_TOOL = 1
|
|
1288
|
+
const ROW_PROXY_SYNC = 2
|
|
1289
|
+
const ROW_PROXY_PORT = 3
|
|
1290
|
+
const ROW_PROXY_CLEANUP = 4
|
|
1291
|
+
const ROW_DAEMON_INSTALL = 5
|
|
1292
|
+
const ROW_DAEMON_RESTART = 6
|
|
1293
|
+
const ROW_DAEMON_STOP = 7
|
|
1294
|
+
const ROW_DAEMON_KILL = 8
|
|
1295
|
+
const ROW_DAEMON_LOGS = 9
|
|
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
|
+
// 📖 Resolve active proxy tool (persisted or fallback to Z-mode)
|
|
1344
|
+
const activeProxyTool = proxySettings.activeTool || state.mode || 'opencode'
|
|
1345
|
+
|
|
1346
|
+
// 📖 Space toggles on proxy rows
|
|
1347
|
+
if (key.name === 'space') {
|
|
1348
|
+
if (state.proxyDaemonCursor === ROW_PROXY_ENABLED) {
|
|
1349
|
+
if (!state.config.settings) state.config.settings = {}
|
|
1350
|
+
state.config.settings.proxy = { ...proxySettings, enabled: !proxySettings.enabled }
|
|
1351
|
+
saveConfig(state.config)
|
|
1352
|
+
state.proxyDaemonMessage = { type: 'success', msg: `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`, ts: Date.now() }
|
|
1353
|
+
return
|
|
1354
|
+
}
|
|
1355
|
+
if (state.proxyDaemonCursor === ROW_PROXY_TOOL) {
|
|
1356
|
+
// 📖 Space also cycles tool (same as Enter on this row)
|
|
1357
|
+
} else if (state.proxyDaemonCursor === ROW_PROXY_SYNC) {
|
|
1358
|
+
if (!state.config.settings) state.config.settings = {}
|
|
1359
|
+
state.config.settings.proxy = { ...proxySettings, syncToOpenCode: !proxySettings.syncToOpenCode }
|
|
1360
|
+
saveConfig(state.config)
|
|
1361
|
+
const { getToolMeta } = await import('./tool-metadata.js')
|
|
1362
|
+
const toolLabel = getToolMeta(activeProxyTool).label
|
|
1363
|
+
state.proxyDaemonMessage = { type: 'success', msg: `✅ Auto-sync to ${toolLabel} ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`, ts: Date.now() }
|
|
1364
|
+
return
|
|
1365
|
+
} else {
|
|
1366
|
+
return
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// 📖 Enter or Space on ROW_PROXY_TOOL → cycle active tool
|
|
1371
|
+
if ((key.name === 'return' || key.name === 'space') && state.proxyDaemonCursor === ROW_PROXY_TOOL) {
|
|
1372
|
+
const { PROXY_SYNCABLE_TOOLS } = await import('./proxy-sync.js')
|
|
1373
|
+
const currentIdx = PROXY_SYNCABLE_TOOLS.indexOf(activeProxyTool)
|
|
1374
|
+
const nextIdx = (currentIdx + 1) % PROXY_SYNCABLE_TOOLS.length
|
|
1375
|
+
const nextTool = PROXY_SYNCABLE_TOOLS[nextIdx]
|
|
1376
|
+
if (!state.config.settings) state.config.settings = {}
|
|
1377
|
+
state.config.settings.proxy = { ...proxySettings, activeTool: nextTool }
|
|
1378
|
+
saveConfig(state.config)
|
|
1379
|
+
const { getToolMeta } = await import('./tool-metadata.js')
|
|
1380
|
+
state.proxyDaemonMessage = { type: 'success', msg: `✅ Active tool: ${getToolMeta(nextTool).emoji} ${getToolMeta(nextTool).label}`, ts: Date.now() }
|
|
1381
|
+
return
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// 📖 Enter on proxy rows
|
|
1385
|
+
if (key.name === 'return') {
|
|
1386
|
+
// 📖 Proxy enabled — toggle
|
|
1387
|
+
if (state.proxyDaemonCursor === ROW_PROXY_ENABLED) {
|
|
1388
|
+
if (!state.config.settings) state.config.settings = {}
|
|
1389
|
+
state.config.settings.proxy = { ...proxySettings, enabled: !proxySettings.enabled }
|
|
1390
|
+
saveConfig(state.config)
|
|
1391
|
+
state.proxyDaemonMessage = { type: 'success', msg: `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`, ts: Date.now() }
|
|
1392
|
+
return
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// 📖 Auto-sync toggle
|
|
1396
|
+
if (state.proxyDaemonCursor === ROW_PROXY_SYNC) {
|
|
1397
|
+
if (!state.config.settings) state.config.settings = {}
|
|
1398
|
+
state.config.settings.proxy = { ...proxySettings, syncToOpenCode: !proxySettings.syncToOpenCode }
|
|
1399
|
+
saveConfig(state.config)
|
|
1400
|
+
const { getToolMeta } = await import('./tool-metadata.js')
|
|
1401
|
+
const toolLabel = getToolMeta(activeProxyTool).label
|
|
1402
|
+
state.proxyDaemonMessage = { type: 'success', msg: `✅ Auto-sync to ${toolLabel} ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`, ts: Date.now() }
|
|
1403
|
+
return
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// 📖 Port — enter edit mode
|
|
1407
|
+
if (state.proxyDaemonCursor === ROW_PROXY_PORT) {
|
|
1408
|
+
state.settingsProxyPortEditMode = true
|
|
1409
|
+
state.settingsProxyPortBuffer = String(proxySettings.preferredPort || 0)
|
|
1410
|
+
return
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// 📖 Clean proxy config — generalized for active tool
|
|
1414
|
+
if (state.proxyDaemonCursor === ROW_PROXY_CLEANUP) {
|
|
1415
|
+
const result = cleanupToolConfig(activeProxyTool)
|
|
1416
|
+
const { getToolMeta } = await import('./tool-metadata.js')
|
|
1417
|
+
const toolLabel = getToolMeta(activeProxyTool).label
|
|
1418
|
+
if (result.success) {
|
|
1419
|
+
state.proxyDaemonMessage = { type: 'success', msg: `✅ ${toolLabel} proxy config cleaned — all fcm-* entries removed`, ts: Date.now() }
|
|
1420
|
+
} else {
|
|
1421
|
+
state.proxyDaemonMessage = { type: 'error', msg: `❌ Cleanup failed: ${result.error}`, ts: Date.now() }
|
|
1422
|
+
}
|
|
1423
|
+
return
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// 📖 Install / Uninstall daemon
|
|
1427
|
+
if (state.proxyDaemonCursor === ROW_DAEMON_INSTALL) {
|
|
1428
|
+
const { getDaemonStatus: _gds, installDaemon, uninstallDaemon, getPlatformSupport } = await import('./daemon-manager.js')
|
|
1429
|
+
const platform = getPlatformSupport()
|
|
1430
|
+
if (!platform.supported) {
|
|
1431
|
+
state.proxyDaemonMessage = { type: 'warning', msg: `⚠ ${platform.reason}`, ts: Date.now() }
|
|
1432
|
+
return
|
|
1433
|
+
}
|
|
1434
|
+
const current = await _gds()
|
|
1435
|
+
if (current.status === 'not-installed') {
|
|
1436
|
+
// 📖 Install daemon
|
|
1437
|
+
if (!proxySettings.enabled) {
|
|
1438
|
+
state.config.settings.proxy.enabled = true
|
|
1439
|
+
}
|
|
1440
|
+
state.config.settings.proxy.daemonEnabled = true
|
|
1441
|
+
state.config.settings.proxy.daemonConsent = new Date().toISOString()
|
|
1442
|
+
if (!state.config.settings.proxy.preferredPort || state.config.settings.proxy.preferredPort === 0) {
|
|
1443
|
+
state.config.settings.proxy.preferredPort = 18045
|
|
1444
|
+
}
|
|
1445
|
+
saveConfig(state.config)
|
|
1446
|
+
const result = installDaemon()
|
|
1447
|
+
if (result.success) {
|
|
1448
|
+
state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 background service installed and started!', ts: Date.now() }
|
|
1449
|
+
const ns = await _gds()
|
|
1450
|
+
state.daemonStatus = ns.status
|
|
1451
|
+
state.daemonInfo = ns.info || null
|
|
1452
|
+
} else {
|
|
1453
|
+
state.proxyDaemonMessage = { type: 'error', msg: `❌ Install failed: ${result.error}`, ts: Date.now() }
|
|
1454
|
+
}
|
|
1455
|
+
} else {
|
|
1456
|
+
// 📖 Uninstall daemon
|
|
1457
|
+
const result = uninstallDaemon()
|
|
1458
|
+
state.config.settings.proxy.daemonEnabled = false
|
|
1459
|
+
saveConfig(state.config)
|
|
1460
|
+
if (result.success) {
|
|
1461
|
+
state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 background service uninstalled.', ts: Date.now() }
|
|
1462
|
+
state.daemonStatus = 'not-installed'
|
|
1463
|
+
state.daemonInfo = null
|
|
1464
|
+
} else {
|
|
1465
|
+
state.proxyDaemonMessage = { type: 'error', msg: `❌ Uninstall failed: ${result.error}`, ts: Date.now() }
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// 📖 Restart daemon
|
|
1472
|
+
if (state.proxyDaemonCursor === ROW_DAEMON_RESTART) {
|
|
1473
|
+
const { restartDaemon, getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1474
|
+
const result = restartDaemon()
|
|
1475
|
+
if (result.success) {
|
|
1476
|
+
state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 service restarted.', ts: Date.now() }
|
|
1477
|
+
// 📖 Wait a bit for the daemon to start up
|
|
1478
|
+
setTimeout(async () => {
|
|
1479
|
+
try {
|
|
1480
|
+
const ns = await _gds()
|
|
1481
|
+
state.daemonStatus = ns.status
|
|
1482
|
+
state.daemonInfo = ns.info || null
|
|
1483
|
+
} catch { /* ignore */ }
|
|
1484
|
+
}, 2000)
|
|
1485
|
+
} else {
|
|
1486
|
+
state.proxyDaemonMessage = { type: 'error', msg: `❌ Restart failed: ${result.error}`, ts: Date.now() }
|
|
1487
|
+
}
|
|
1488
|
+
return
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// 📖 Stop daemon (SIGTERM)
|
|
1492
|
+
if (state.proxyDaemonCursor === ROW_DAEMON_STOP) {
|
|
1493
|
+
const { stopDaemon, getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1494
|
+
const result = stopDaemon()
|
|
1495
|
+
if (result.success) {
|
|
1496
|
+
const warning = result.willRestart ? ' (service may auto-restart it)' : ''
|
|
1497
|
+
state.proxyDaemonMessage = { type: 'success', msg: `✅ FCM Proxy V2 service stopped.${warning}`, ts: Date.now() }
|
|
1498
|
+
setTimeout(async () => {
|
|
1499
|
+
try {
|
|
1500
|
+
const ns = await _gds()
|
|
1501
|
+
state.daemonStatus = ns.status
|
|
1502
|
+
state.daemonInfo = ns.info || null
|
|
1503
|
+
} catch { /* ignore */ }
|
|
1504
|
+
}, 1500)
|
|
1505
|
+
} else {
|
|
1506
|
+
state.proxyDaemonMessage = { type: 'error', msg: `❌ Stop failed: ${result.error}`, ts: Date.now() }
|
|
1507
|
+
}
|
|
1508
|
+
return
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// 📖 Force kill daemon (SIGKILL) — emergency
|
|
1512
|
+
if (state.proxyDaemonCursor === ROW_DAEMON_KILL) {
|
|
1513
|
+
const { killDaemonProcess, getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1514
|
+
const result = killDaemonProcess()
|
|
1515
|
+
if (result.success) {
|
|
1516
|
+
state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 service force-killed (SIGKILL).', ts: Date.now() }
|
|
1517
|
+
const ns = await _gds()
|
|
1518
|
+
state.daemonStatus = ns.status
|
|
1519
|
+
state.daemonInfo = ns.info || null
|
|
1520
|
+
} else {
|
|
1521
|
+
state.proxyDaemonMessage = { type: 'error', msg: `❌ Kill failed: ${result.error}`, ts: Date.now() }
|
|
1522
|
+
}
|
|
1523
|
+
return
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// 📖 View daemon logs
|
|
1527
|
+
if (state.proxyDaemonCursor === ROW_DAEMON_LOGS) {
|
|
1528
|
+
const { getDaemonLogPath } = await import('./daemon-manager.js')
|
|
1529
|
+
const logPath = getDaemonLogPath()
|
|
1530
|
+
try {
|
|
1531
|
+
const { readFileSync, existsSync } = await import('node:fs')
|
|
1532
|
+
if (!existsSync(logPath)) {
|
|
1533
|
+
state.proxyDaemonMessage = { type: 'warning', msg: `⚠ No log file found at ${logPath}`, ts: Date.now() }
|
|
1534
|
+
return
|
|
1535
|
+
}
|
|
1536
|
+
const content = readFileSync(logPath, 'utf8')
|
|
1537
|
+
const logLines = content.split('\n')
|
|
1538
|
+
const last50 = logLines.slice(-50).join('\n')
|
|
1539
|
+
// 📖 Display in the log overlay (repurpose log view)
|
|
1540
|
+
state.proxyDaemonOpen = false
|
|
1541
|
+
state.logVisible = true
|
|
1542
|
+
state.logScrollOffset = 0
|
|
1543
|
+
state._daemonLogContent = last50
|
|
1544
|
+
state.proxyDaemonMessage = { type: 'success', msg: `📖 Showing last ${Math.min(50, logLines.length)} lines from ${logPath}`, ts: Date.now() }
|
|
1545
|
+
} catch (err) {
|
|
1546
|
+
state.proxyDaemonMessage = { type: 'error', msg: `❌ Could not read logs: ${err.message}`, ts: Date.now() }
|
|
1547
|
+
}
|
|
1548
|
+
return
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
return // 📖 Swallow all other keys while proxy/daemon overlay is open
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1390
1555
|
// 📖 P key: open settings screen
|
|
1391
1556
|
if (key.name === 'p' && !key.shift) {
|
|
1392
1557
|
state.settingsOpen = true
|
|
@@ -1397,6 +1562,13 @@ export function createKeyHandler(ctx) {
|
|
|
1397
1562
|
state.settingsProxyPortBuffer = ''
|
|
1398
1563
|
state.settingsEditBuffer = ''
|
|
1399
1564
|
state.settingsScrollOffset = 0
|
|
1565
|
+
// 📖 Refresh daemon status when opening settings
|
|
1566
|
+
import('./daemon-manager.js').then(dm => {
|
|
1567
|
+
dm.getDaemonStatus().then(s => {
|
|
1568
|
+
state.daemonStatus = s.status
|
|
1569
|
+
state.daemonInfo = s.info || null
|
|
1570
|
+
}).catch(() => {})
|
|
1571
|
+
}).catch(() => {})
|
|
1400
1572
|
return
|
|
1401
1573
|
}
|
|
1402
1574
|
|
|
@@ -1593,18 +1765,25 @@ export function createKeyHandler(ctx) {
|
|
|
1593
1765
|
return
|
|
1594
1766
|
}
|
|
1595
1767
|
|
|
1596
|
-
// 📖 J key: open
|
|
1768
|
+
// 📖 J key: open FCM Proxy V2 settings overlay directly (bypasses Settings screen)
|
|
1597
1769
|
if (key.name === 'j') {
|
|
1598
|
-
state.
|
|
1599
|
-
state.
|
|
1600
|
-
state.
|
|
1601
|
-
state.
|
|
1770
|
+
state.proxyDaemonOpen = true
|
|
1771
|
+
state.proxyDaemonCursor = 0
|
|
1772
|
+
state.proxyDaemonScrollOffset = 0
|
|
1773
|
+
state.proxyDaemonMessage = null
|
|
1774
|
+
// 📖 Refresh daemon status when entering
|
|
1775
|
+
try {
|
|
1776
|
+
const { getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1777
|
+
const st = await _gds()
|
|
1778
|
+
state.daemonStatus = st.status
|
|
1779
|
+
state.daemonInfo = st.info || null
|
|
1780
|
+
} catch { /* ignore */ }
|
|
1602
1781
|
return
|
|
1603
1782
|
}
|
|
1604
1783
|
|
|
1605
|
-
// 📖 I key: open
|
|
1784
|
+
// 📖 I key: open Feedback overlay (anonymous Discord feedback)
|
|
1606
1785
|
if (key.name === 'i') {
|
|
1607
|
-
state.
|
|
1786
|
+
state.feedbackOpen = true
|
|
1608
1787
|
state.bugReportBuffer = ''
|
|
1609
1788
|
state.bugReportStatus = 'idle'
|
|
1610
1789
|
state.bugReportError = null
|