free-coding-models 0.3.11 → 0.3.13
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 +24 -0
- package/README.md +112 -1134
- package/bin/free-coding-models.js +18 -170
- package/package.json +2 -3
- package/src/cli-help.js +0 -18
- package/src/config.js +5 -117
- package/src/endpoint-installer.js +26 -64
- package/src/key-handler.js +90 -443
- package/src/legacy-proxy-cleanup.js +432 -0
- package/src/openclaw.js +69 -108
- package/src/opencode-config.js +48 -0
- package/src/opencode.js +6 -248
- package/src/overlays.js +28 -520
- package/src/product-flags.js +14 -0
- package/src/render-helpers.js +2 -34
- package/src/render-table.js +11 -19
- package/src/testfcm.js +90 -43
- package/src/token-usage-reader.js +9 -38
- package/src/tool-launchers.js +235 -409
- package/src/tool-metadata.js +0 -7
- package/src/utils.js +3 -68
- package/bin/fcm-proxy-daemon.js +0 -242
- package/src/account-manager.js +0 -634
- package/src/anthropic-translator.js +0 -440
- package/src/daemon-manager.js +0 -527
- package/src/error-classifier.js +0 -157
- package/src/log-reader.js +0 -195
- package/src/opencode-sync.js +0 -200
- package/src/proxy-foreground.js +0 -234
- package/src/proxy-server.js +0 -1506
- package/src/proxy-sync.js +0 -591
- package/src/proxy-topology.js +0 -85
- package/src/request-transformer.js +0 -180
- package/src/responses-translator.js +0 -423
- package/src/token-stats.js +0 -320
package/src/key-handler.js
CHANGED
|
@@ -8,10 +8,7 @@
|
|
|
8
8
|
* tool launch actions. It also keeps the live key bindings aligned with the
|
|
9
9
|
* highlighted letters shown in the table headers.
|
|
10
10
|
*
|
|
11
|
-
* 📖 Key J opens the FCM Proxy V2 overlay directly (with daemon status refresh).
|
|
12
11
|
* 📖 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.
|
|
15
12
|
*
|
|
16
13
|
* It also owns the "test key" model selection used by the Settings overlay.
|
|
17
14
|
* Some providers expose models in `/v1/models` that are not actually callable
|
|
@@ -31,7 +28,8 @@
|
|
|
31
28
|
*/
|
|
32
29
|
|
|
33
30
|
import { loadChangelog } from './changelog-loader.js'
|
|
34
|
-
import {
|
|
31
|
+
import { loadConfig, replaceConfigContents } from './config.js'
|
|
32
|
+
import { cleanupLegacyProxyArtifacts } from './legacy-proxy-cleanup.js'
|
|
35
33
|
|
|
36
34
|
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
37
35
|
// 📖 is not guaranteed to be accepted by their chat endpoint.
|
|
@@ -160,7 +158,6 @@ export function createKeyHandler(ctx) {
|
|
|
160
158
|
MODELS,
|
|
161
159
|
sources,
|
|
162
160
|
getApiKey,
|
|
163
|
-
getProxySettings,
|
|
164
161
|
resolveApiKeys,
|
|
165
162
|
addApiKey,
|
|
166
163
|
removeApiKey,
|
|
@@ -171,7 +168,6 @@ export function createKeyHandler(ctx) {
|
|
|
171
168
|
getInstallTargetModes,
|
|
172
169
|
getProviderCatalogModels,
|
|
173
170
|
installProviderEndpoints,
|
|
174
|
-
CONNECTION_MODES,
|
|
175
171
|
syncFavoriteFlags,
|
|
176
172
|
toggleFavoriteModel,
|
|
177
173
|
sortResultsWithPinnedFavorites,
|
|
@@ -181,19 +177,12 @@ export function createKeyHandler(ctx) {
|
|
|
181
177
|
TIER_CYCLE,
|
|
182
178
|
ORIGIN_CYCLE,
|
|
183
179
|
ENV_VAR_NAMES,
|
|
184
|
-
ensureProxyRunning,
|
|
185
|
-
syncToOpenCode,
|
|
186
|
-
cleanupToolConfig,
|
|
187
|
-
restoreOpenCodeBackup,
|
|
188
180
|
checkForUpdateDetailed,
|
|
189
181
|
runUpdate,
|
|
190
182
|
startOpenClaw,
|
|
191
183
|
startOpenCodeDesktop,
|
|
192
184
|
startOpenCode,
|
|
193
|
-
startProxyAndLaunch,
|
|
194
185
|
startExternalTool,
|
|
195
|
-
buildProxyTopologyFromConfig,
|
|
196
|
-
isProxyEnabledForConfig,
|
|
197
186
|
getToolModeOrder,
|
|
198
187
|
startRecommendAnalysis,
|
|
199
188
|
stopRecommendAnalysis,
|
|
@@ -206,7 +195,6 @@ export function createKeyHandler(ctx) {
|
|
|
206
195
|
CONTEXT_BUDGETS,
|
|
207
196
|
toFavoriteKey,
|
|
208
197
|
mergedModels,
|
|
209
|
-
apiKey,
|
|
210
198
|
chalk,
|
|
211
199
|
setPingMode,
|
|
212
200
|
noteUserActivity,
|
|
@@ -329,6 +317,71 @@ export function createKeyHandler(ctx) {
|
|
|
329
317
|
runUpdate(latestVersion)
|
|
330
318
|
}
|
|
331
319
|
|
|
320
|
+
// 📖 The old multi-tool proxy is discontinued. This maintenance action clears
|
|
321
|
+
// 📖 stale config/env/service leftovers so users stay on the stable direct path.
|
|
322
|
+
function runLegacyProxyCleanup() {
|
|
323
|
+
const summary = cleanupLegacyProxyArtifacts()
|
|
324
|
+
replaceConfigContents(state.config, loadConfig())
|
|
325
|
+
|
|
326
|
+
if (summary.errors.length > 0) {
|
|
327
|
+
const cleanedTargets = summary.removedFiles.length + summary.updatedFiles.length
|
|
328
|
+
const partialDetail = summary.changed
|
|
329
|
+
? `Cleaned ${cleanedTargets} legacy paths, but ${summary.errors.length} items still need manual cleanup.`
|
|
330
|
+
: `Cleanup hit ${summary.errors.length} file errors.`
|
|
331
|
+
state.settingsSyncStatus = {
|
|
332
|
+
type: 'error',
|
|
333
|
+
msg: `⚠️ Proxy cleanup was partial. ${partialDetail} The old bridge is discontinued while a more stable replacement is being built.`,
|
|
334
|
+
}
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (summary.changed) {
|
|
339
|
+
const cleanedTargets = summary.removedFiles.length + summary.updatedFiles.length
|
|
340
|
+
state.settingsSyncStatus = {
|
|
341
|
+
type: 'success',
|
|
342
|
+
msg: `ℹ️ Removed discontinued proxy leftovers from ${cleanedTargets} path${cleanedTargets === 1 ? '' : 's'}. A much more stable replacement is coming soon.`,
|
|
343
|
+
}
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
state.settingsSyncStatus = {
|
|
348
|
+
type: 'success',
|
|
349
|
+
msg: 'ℹ️ No discontinued proxy config was found. You are already on the stable direct-provider setup.',
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 📖 Keep the width-warning runtime state synced with the persisted Settings toggle
|
|
354
|
+
// 📖 so the overlay reacts immediately when the user enables or disables it.
|
|
355
|
+
function syncWidthsWarningState() {
|
|
356
|
+
const widthsWarningDisabled = state.config.settings?.disableWidthsWarning === true
|
|
357
|
+
state.disableWidthsWarning = widthsWarningDisabled
|
|
358
|
+
|
|
359
|
+
if (widthsWarningDisabled) {
|
|
360
|
+
state.widthWarningStartedAt = null
|
|
361
|
+
state.widthWarningDismissed = false
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
state.widthWarningShowCount = 0
|
|
366
|
+
if ((state.terminalCols || 80) < 166) {
|
|
367
|
+
state.widthWarningStartedAt = Date.now()
|
|
368
|
+
state.widthWarningDismissed = false
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
state.widthWarningStartedAt = null
|
|
373
|
+
state.widthWarningDismissed = false
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 📖 Toggle the width-warning setting and apply the effect immediately instead
|
|
377
|
+
// 📖 of waiting for a resize or restart.
|
|
378
|
+
function toggleWidthsWarningSetting() {
|
|
379
|
+
if (!state.config.settings) state.config.settings = {}
|
|
380
|
+
state.config.settings.disableWidthsWarning = !state.config.settings.disableWidthsWarning
|
|
381
|
+
syncWidthsWarningState()
|
|
382
|
+
saveConfig(state.config)
|
|
383
|
+
}
|
|
384
|
+
|
|
332
385
|
function resetInstallEndpointsOverlay() {
|
|
333
386
|
state.installEndpointsOpen = false
|
|
334
387
|
state.installEndpointsPhase = 'providers'
|
|
@@ -385,7 +438,6 @@ export function createKeyHandler(ctx) {
|
|
|
385
438
|
|
|
386
439
|
const providerChoices = getConfiguredInstallableProviders(state.config)
|
|
387
440
|
const toolChoices = getInstallTargetModes()
|
|
388
|
-
const connectionChoices = CONNECTION_MODES || []
|
|
389
441
|
const modelChoices = state.installEndpointsProviderKey
|
|
390
442
|
? getProviderCatalogModels(state.installEndpointsProviderKey)
|
|
391
443
|
: []
|
|
@@ -394,7 +446,6 @@ export function createKeyHandler(ctx) {
|
|
|
394
446
|
const maxIndexByPhase = () => {
|
|
395
447
|
if (state.installEndpointsPhase === 'providers') return Math.max(0, providerChoices.length - 1)
|
|
396
448
|
if (state.installEndpointsPhase === 'tools') return Math.max(0, toolChoices.length - 1)
|
|
397
|
-
if (state.installEndpointsPhase === 'connection') return Math.max(0, connectionChoices.length - 1)
|
|
398
449
|
if (state.installEndpointsPhase === 'scope') return 1
|
|
399
450
|
if (state.installEndpointsPhase === 'models') return Math.max(0, modelChoices.length - 1)
|
|
400
451
|
return 0
|
|
@@ -437,18 +488,12 @@ export function createKeyHandler(ctx) {
|
|
|
437
488
|
state.installEndpointsScrollOffset = 0
|
|
438
489
|
return
|
|
439
490
|
}
|
|
440
|
-
if (state.installEndpointsPhase === '
|
|
491
|
+
if (state.installEndpointsPhase === 'scope') {
|
|
441
492
|
state.installEndpointsPhase = 'tools'
|
|
442
493
|
state.installEndpointsCursor = 0
|
|
443
494
|
state.installEndpointsScrollOffset = 0
|
|
444
495
|
return
|
|
445
496
|
}
|
|
446
|
-
if (state.installEndpointsPhase === 'scope') {
|
|
447
|
-
state.installEndpointsPhase = 'connection'
|
|
448
|
-
state.installEndpointsCursor = state.installEndpointsConnectionMode === 'proxy' ? 1 : 0
|
|
449
|
-
state.installEndpointsScrollOffset = 0
|
|
450
|
-
return
|
|
451
|
-
}
|
|
452
497
|
if (state.installEndpointsPhase === 'models') {
|
|
453
498
|
state.installEndpointsPhase = 'scope'
|
|
454
499
|
state.installEndpointsCursor = state.installEndpointsScope === 'selected' ? 1 : 0
|
|
@@ -481,20 +526,7 @@ export function createKeyHandler(ctx) {
|
|
|
481
526
|
const selectedToolMode = toolChoices[state.installEndpointsCursor]
|
|
482
527
|
if (!selectedToolMode) return
|
|
483
528
|
state.installEndpointsToolMode = selectedToolMode
|
|
484
|
-
state.
|
|
485
|
-
state.installEndpointsCursor = 0
|
|
486
|
-
state.installEndpointsScrollOffset = 0
|
|
487
|
-
state.installEndpointsErrorMsg = null
|
|
488
|
-
}
|
|
489
|
-
return
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// 📖 Connection mode phase: Direct Provider vs FCM Proxy
|
|
493
|
-
if (state.installEndpointsPhase === 'connection') {
|
|
494
|
-
if (key.name === 'return') {
|
|
495
|
-
const selected = connectionChoices[state.installEndpointsCursor]
|
|
496
|
-
if (!selected) return
|
|
497
|
-
state.installEndpointsConnectionMode = selected.key
|
|
529
|
+
state.installEndpointsConnectionMode = 'direct'
|
|
498
530
|
state.installEndpointsPhase = 'scope'
|
|
499
531
|
state.installEndpointsCursor = 0
|
|
500
532
|
state.installEndpointsScrollOffset = 0
|
|
@@ -665,29 +697,6 @@ export function createKeyHandler(ctx) {
|
|
|
665
697
|
return
|
|
666
698
|
}
|
|
667
699
|
|
|
668
|
-
// 📖 Log page overlay: full keyboard navigation + key swallowing while overlay is open.
|
|
669
|
-
if (state.logVisible) {
|
|
670
|
-
const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
|
|
671
|
-
if (key.name === 'escape' || key.name === 'x') {
|
|
672
|
-
state.logVisible = false
|
|
673
|
-
return
|
|
674
|
-
}
|
|
675
|
-
// 📖 A key: toggle between showing all logs and limited to 500
|
|
676
|
-
if (key.name === 'a') {
|
|
677
|
-
state.logShowAll = !state.logShowAll
|
|
678
|
-
state.logScrollOffset = 0
|
|
679
|
-
return
|
|
680
|
-
}
|
|
681
|
-
if (key.name === 'up') { state.logScrollOffset = Math.max(0, state.logScrollOffset - 1); return }
|
|
682
|
-
if (key.name === 'down') { state.logScrollOffset += 1; return }
|
|
683
|
-
if (key.name === 'pageup') { state.logScrollOffset = Math.max(0, state.logScrollOffset - pageStep); return }
|
|
684
|
-
if (key.name === 'pagedown') { state.logScrollOffset += pageStep; return }
|
|
685
|
-
if (key.name === 'home') { state.logScrollOffset = 0; return }
|
|
686
|
-
if (key.name === 'end') { state.logScrollOffset = Number.MAX_SAFE_INTEGER; return }
|
|
687
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
688
|
-
return
|
|
689
|
-
}
|
|
690
|
-
|
|
691
700
|
// 📖 Changelog overlay: two-phase (index + details) with keyboard navigation
|
|
692
701
|
if (state.changelogOpen) {
|
|
693
702
|
const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
|
|
@@ -914,14 +923,13 @@ export function createKeyHandler(ctx) {
|
|
|
914
923
|
|
|
915
924
|
// ─── Settings overlay keyboard handling ───────────────────────────────────
|
|
916
925
|
if (state.settingsOpen) {
|
|
917
|
-
const proxySettings = getProxySettings(state.config)
|
|
918
926
|
const providerKeys = Object.keys(sources)
|
|
919
|
-
const updateRowIdx = providerKeys.length
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
927
|
+
const updateRowIdx = providerKeys.length
|
|
928
|
+
const widthWarningRowIdx = updateRowIdx + 1
|
|
929
|
+
const cleanupLegacyProxyRowIdx = widthWarningRowIdx + 1
|
|
930
|
+
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
923
931
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
924
|
-
|
|
932
|
+
const maxRowIdx = changelogViewRowIdx
|
|
925
933
|
|
|
926
934
|
// 📖 Edit/Add-key mode: capture typed characters for the API key
|
|
927
935
|
if (state.settingsEditMode || state.settingsAddKeyMode) {
|
|
@@ -982,8 +990,6 @@ const updateRowIdx = providerKeys.length
|
|
|
982
990
|
state.settingsOpen = false
|
|
983
991
|
state.settingsEditMode = false
|
|
984
992
|
state.settingsAddKeyMode = false
|
|
985
|
-
state.settingsProxyPortEditMode = false
|
|
986
|
-
state.settingsProxyPortBuffer = ''
|
|
987
993
|
state.settingsEditBuffer = ''
|
|
988
994
|
state.settingsSyncStatus = null // 📖 Clear sync status on close
|
|
989
995
|
// 📖 Rebuild results: add models from newly enabled providers, remove disabled
|
|
@@ -1066,26 +1072,12 @@ const updateRowIdx = providerKeys.length
|
|
|
1066
1072
|
|
|
1067
1073
|
// 📖 Widths Warning toggle (Enter to toggle)
|
|
1068
1074
|
if (state.settingsCursor === widthWarningRowIdx) {
|
|
1069
|
-
|
|
1070
|
-
state.config.settings.disableWidthsWarning = !state.config.settings.disableWidthsWarning
|
|
1071
|
-
saveConfig(state.config)
|
|
1075
|
+
toggleWidthsWarningSetting()
|
|
1072
1076
|
return
|
|
1073
1077
|
}
|
|
1074
1078
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
state.settingsOpen = false
|
|
1078
|
-
state.proxyDaemonOpen = true
|
|
1079
|
-
state.proxyDaemonCursor = 0
|
|
1080
|
-
state.proxyDaemonScrollOffset = 0
|
|
1081
|
-
state.proxyDaemonMessage = null
|
|
1082
|
-
// 📖 Refresh daemon status when entering
|
|
1083
|
-
try {
|
|
1084
|
-
const { getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1085
|
-
const st = await _gds()
|
|
1086
|
-
state.daemonStatus = st.status
|
|
1087
|
-
state.daemonInfo = st.info || null
|
|
1088
|
-
} catch { /* ignore */ }
|
|
1079
|
+
if (state.settingsCursor === cleanupLegacyProxyRowIdx) {
|
|
1080
|
+
runLegacyProxyCleanup()
|
|
1089
1081
|
return
|
|
1090
1082
|
}
|
|
1091
1083
|
|
|
@@ -1111,12 +1103,14 @@ const updateRowIdx = providerKeys.length
|
|
|
1111
1103
|
|
|
1112
1104
|
if (key.name === 'space') {
|
|
1113
1105
|
// 📖 Exclude certain rows from space toggle
|
|
1114
|
-
if (
|
|
1106
|
+
if (
|
|
1107
|
+
state.settingsCursor === updateRowIdx
|
|
1108
|
+
|| state.settingsCursor === cleanupLegacyProxyRowIdx
|
|
1109
|
+
|| state.settingsCursor === changelogViewRowIdx
|
|
1110
|
+
) return
|
|
1115
1111
|
// 📖 Widths Warning toggle (disable/enable)
|
|
1116
1112
|
if (state.settingsCursor === widthWarningRowIdx) {
|
|
1117
|
-
|
|
1118
|
-
state.config.settings.disableWidthsWarning = !state.config.settings.disableWidthsWarning
|
|
1119
|
-
saveConfig(state.config)
|
|
1113
|
+
toggleWidthsWarningSetting()
|
|
1120
1114
|
return
|
|
1121
1115
|
}
|
|
1122
1116
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
@@ -1131,7 +1125,11 @@ const updateRowIdx = providerKeys.length
|
|
|
1131
1125
|
}
|
|
1132
1126
|
|
|
1133
1127
|
if (key.name === 't') {
|
|
1134
|
-
if (
|
|
1128
|
+
if (
|
|
1129
|
+
state.settingsCursor === updateRowIdx
|
|
1130
|
+
|| state.settingsCursor === cleanupLegacyProxyRowIdx
|
|
1131
|
+
|| state.settingsCursor === changelogViewRowIdx
|
|
1132
|
+
) return
|
|
1135
1133
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1136
1134
|
|
|
1137
1135
|
// 📖 Test the selected provider's key (fires a real ping)
|
|
@@ -1149,47 +1147,6 @@ const updateRowIdx = providerKeys.length
|
|
|
1149
1147
|
|
|
1150
1148
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1151
1149
|
|
|
1152
|
-
// 📖 S key: sync FCM provider entries to OpenCode config (merge, don't replace)
|
|
1153
|
-
if (key.name === 's' && !key.shift && !key.ctrl) {
|
|
1154
|
-
try {
|
|
1155
|
-
if (!proxySettings.enabled) {
|
|
1156
|
-
state.settingsSyncStatus = { type: 'error', msg: '⚠ Enable Proxy mode first if you want to sync fcm-proxy into OpenCode' }
|
|
1157
|
-
return
|
|
1158
|
-
}
|
|
1159
|
-
if (!proxySettings.syncToOpenCode) {
|
|
1160
|
-
state.settingsSyncStatus = { type: 'error', msg: '⚠ Enable "Persist proxy in OpenCode" first, or use the direct OpenCode flow only' }
|
|
1161
|
-
return
|
|
1162
|
-
}
|
|
1163
|
-
// 📖 Sync now also ensures proxy is running, so OpenCode can use fcm-proxy immediately.
|
|
1164
|
-
const started = await ensureProxyRunning(state.config)
|
|
1165
|
-
const result = syncToOpenCode(state.config, sources, mergedModels, {
|
|
1166
|
-
proxyPort: started.port,
|
|
1167
|
-
proxyToken: started.proxyToken,
|
|
1168
|
-
availableModelSlugs: started.availableModelSlugs,
|
|
1169
|
-
})
|
|
1170
|
-
state.settingsSyncStatus = {
|
|
1171
|
-
type: 'success',
|
|
1172
|
-
msg: `✅ Synced ${result.providerKey} (${result.modelCount} models), proxy running on :${started.port}`,
|
|
1173
|
-
}
|
|
1174
|
-
} catch (err) {
|
|
1175
|
-
state.settingsSyncStatus = { type: 'error', msg: `❌ Sync failed: ${err.message}` }
|
|
1176
|
-
}
|
|
1177
|
-
return
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
// 📖 R key: restore OpenCode config from backup (opencode.json.bak)
|
|
1181
|
-
if (key.name === 'r' && !key.shift && !key.ctrl) {
|
|
1182
|
-
try {
|
|
1183
|
-
const restored = restoreOpenCodeBackup()
|
|
1184
|
-
state.settingsSyncStatus = restored
|
|
1185
|
-
? { type: 'success', msg: '✅ OpenCode config restored from backup' }
|
|
1186
|
-
: { type: 'error', msg: '⚠ No backup found (opencode.json.bak)' }
|
|
1187
|
-
} catch (err) {
|
|
1188
|
-
state.settingsSyncStatus = { type: 'error', msg: `❌ Restore failed: ${err.message}` }
|
|
1189
|
-
}
|
|
1190
|
-
return
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
1150
|
// 📖 + key: open add-key input (empty buffer) — appends new key on Enter
|
|
1194
1151
|
if ((str === '+' || key.name === '+') && state.settingsCursor < providerKeys.length) {
|
|
1195
1152
|
state.settingsEditBuffer = '' // 📖 Start with empty buffer (not existing key)
|
|
@@ -1220,291 +1177,14 @@ const updateRowIdx = providerKeys.length
|
|
|
1220
1177
|
return // 📖 Swallow all other keys while settings is open
|
|
1221
1178
|
}
|
|
1222
1179
|
|
|
1223
|
-
// ─── Proxy & Daemon overlay keyboard handling ─────────────────────────────
|
|
1224
|
-
if (state.proxyDaemonOpen) {
|
|
1225
|
-
const proxySettings = getProxySettings(state.config)
|
|
1226
|
-
const ROW_PROXY_ENABLED = 0
|
|
1227
|
-
const ROW_PROXY_SYNC = 1
|
|
1228
|
-
const ROW_PROXY_PORT = 2
|
|
1229
|
-
const ROW_PROXY_CLEANUP = 3
|
|
1230
|
-
const ROW_DAEMON_INSTALL = 4
|
|
1231
|
-
const ROW_DAEMON_RESTART = 5
|
|
1232
|
-
const ROW_DAEMON_STOP = 6
|
|
1233
|
-
const ROW_DAEMON_KILL = 7
|
|
1234
|
-
const ROW_DAEMON_LOGS = 8
|
|
1235
|
-
|
|
1236
|
-
const daemonStatus = state.daemonStatus || 'not-installed'
|
|
1237
|
-
const daemonIsInstalled = daemonStatus === 'running' || daemonStatus === 'stopped' || daemonStatus === 'unhealthy' || daemonStatus === 'stale'
|
|
1238
|
-
const maxRow = daemonIsInstalled ? ROW_DAEMON_LOGS : ROW_DAEMON_INSTALL
|
|
1239
|
-
|
|
1240
|
-
// 📖 Port edit mode (same as old Settings behavior)
|
|
1241
|
-
if (state.settingsProxyPortEditMode) {
|
|
1242
|
-
if (key.name === 'return') {
|
|
1243
|
-
const parsed = parseInt(state.settingsProxyPortBuffer, 10)
|
|
1244
|
-
if (isNaN(parsed) || parsed < 0 || parsed > 65535) {
|
|
1245
|
-
state.proxyDaemonMessage = { type: 'error', msg: '❌ Port must be 0 (auto) or 1–65535', ts: Date.now() }
|
|
1246
|
-
return
|
|
1247
|
-
}
|
|
1248
|
-
if (!state.config.settings) state.config.settings = {}
|
|
1249
|
-
state.config.settings.proxy = { ...proxySettings, preferredPort: parsed }
|
|
1250
|
-
saveConfig(state.config)
|
|
1251
|
-
state.settingsProxyPortEditMode = false
|
|
1252
|
-
state.settingsProxyPortBuffer = ''
|
|
1253
|
-
state.proxyDaemonMessage = { type: 'success', msg: `✅ Preferred port saved: ${parsed === 0 ? 'auto' : parsed}`, ts: Date.now() }
|
|
1254
|
-
} else if (key.name === 'escape') {
|
|
1255
|
-
state.settingsProxyPortEditMode = false
|
|
1256
|
-
state.settingsProxyPortBuffer = ''
|
|
1257
|
-
} else if (key.name === 'backspace') {
|
|
1258
|
-
state.settingsProxyPortBuffer = state.settingsProxyPortBuffer.slice(0, -1)
|
|
1259
|
-
} else if (str && /^[0-9]$/.test(str) && state.settingsProxyPortBuffer.length < 5) {
|
|
1260
|
-
state.settingsProxyPortBuffer += str
|
|
1261
|
-
}
|
|
1262
|
-
return
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
// 📖 Escape → back to Settings
|
|
1266
|
-
if (key.name === 'escape') {
|
|
1267
|
-
state.proxyDaemonOpen = false
|
|
1268
|
-
state.settingsOpen = true
|
|
1269
|
-
state.settingsProxyPortEditMode = false
|
|
1270
|
-
state.settingsProxyPortBuffer = ''
|
|
1271
|
-
return
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
// 📖 Navigation
|
|
1275
|
-
if (key.name === 'up' && state.proxyDaemonCursor > 0) { state.proxyDaemonCursor--; return }
|
|
1276
|
-
if (key.name === 'down' && state.proxyDaemonCursor < maxRow) { state.proxyDaemonCursor++; return }
|
|
1277
|
-
if (key.name === 'home') { state.proxyDaemonCursor = 0; return }
|
|
1278
|
-
if (key.name === 'end') { state.proxyDaemonCursor = maxRow; return }
|
|
1279
|
-
if (key.name === 'pageup') { state.proxyDaemonCursor = Math.max(0, state.proxyDaemonCursor - 5); return }
|
|
1280
|
-
if (key.name === 'pagedown') { state.proxyDaemonCursor = Math.min(maxRow, state.proxyDaemonCursor + 5); return }
|
|
1281
|
-
|
|
1282
|
-
// 📖 Proxy sync now follows the current Z-selected tool automatically.
|
|
1283
|
-
const currentToolMode = state.mode || 'opencode'
|
|
1284
|
-
const currentProxyTool = resolveProxySyncToolMode(currentToolMode)
|
|
1285
|
-
|
|
1286
|
-
// 📖 Space toggles on proxy rows
|
|
1287
|
-
if (key.name === 'space') {
|
|
1288
|
-
if (state.proxyDaemonCursor === ROW_PROXY_ENABLED) {
|
|
1289
|
-
if (!state.config.settings) state.config.settings = {}
|
|
1290
|
-
state.config.settings.proxy = { ...proxySettings, enabled: !proxySettings.enabled }
|
|
1291
|
-
saveConfig(state.config)
|
|
1292
|
-
state.proxyDaemonMessage = { type: 'success', msg: `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`, ts: Date.now() }
|
|
1293
|
-
return
|
|
1294
|
-
}
|
|
1295
|
-
if (state.proxyDaemonCursor === ROW_PROXY_SYNC) {
|
|
1296
|
-
if (!currentProxyTool) {
|
|
1297
|
-
state.proxyDaemonMessage = { type: 'warning', msg: '⚠ Current tool does not support persisted proxy sync', ts: Date.now() }
|
|
1298
|
-
return
|
|
1299
|
-
}
|
|
1300
|
-
if (!state.config.settings) state.config.settings = {}
|
|
1301
|
-
state.config.settings.proxy = { ...proxySettings, syncToOpenCode: !proxySettings.syncToOpenCode }
|
|
1302
|
-
saveConfig(state.config)
|
|
1303
|
-
const { getToolMeta } = await import('./tool-metadata.js')
|
|
1304
|
-
const toolLabel = getToolMeta(currentProxyTool).label
|
|
1305
|
-
state.proxyDaemonMessage = { type: 'success', msg: `✅ Auto-sync to ${toolLabel} ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`, ts: Date.now() }
|
|
1306
|
-
return
|
|
1307
|
-
} else {
|
|
1308
|
-
return
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// 📖 Enter on proxy rows
|
|
1313
|
-
if (key.name === 'return') {
|
|
1314
|
-
// 📖 Proxy enabled — toggle
|
|
1315
|
-
if (state.proxyDaemonCursor === ROW_PROXY_ENABLED) {
|
|
1316
|
-
if (!state.config.settings) state.config.settings = {}
|
|
1317
|
-
state.config.settings.proxy = { ...proxySettings, enabled: !proxySettings.enabled }
|
|
1318
|
-
saveConfig(state.config)
|
|
1319
|
-
state.proxyDaemonMessage = { type: 'success', msg: `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`, ts: Date.now() }
|
|
1320
|
-
return
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
// 📖 Auto-sync toggle
|
|
1324
|
-
if (state.proxyDaemonCursor === ROW_PROXY_SYNC) {
|
|
1325
|
-
if (!currentProxyTool) {
|
|
1326
|
-
state.proxyDaemonMessage = { type: 'warning', msg: '⚠ Current tool does not support persisted proxy sync', ts: Date.now() }
|
|
1327
|
-
return
|
|
1328
|
-
}
|
|
1329
|
-
if (!state.config.settings) state.config.settings = {}
|
|
1330
|
-
state.config.settings.proxy = { ...proxySettings, syncToOpenCode: !proxySettings.syncToOpenCode }
|
|
1331
|
-
saveConfig(state.config)
|
|
1332
|
-
const { getToolMeta } = await import('./tool-metadata.js')
|
|
1333
|
-
const toolLabel = getToolMeta(currentProxyTool).label
|
|
1334
|
-
state.proxyDaemonMessage = { type: 'success', msg: `✅ Auto-sync to ${toolLabel} ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`, ts: Date.now() }
|
|
1335
|
-
return
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
// 📖 Port — enter edit mode
|
|
1339
|
-
if (state.proxyDaemonCursor === ROW_PROXY_PORT) {
|
|
1340
|
-
state.settingsProxyPortEditMode = true
|
|
1341
|
-
state.settingsProxyPortBuffer = String(proxySettings.preferredPort || 0)
|
|
1342
|
-
return
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
// 📖 Clean proxy config — generalized for active tool
|
|
1346
|
-
if (state.proxyDaemonCursor === ROW_PROXY_CLEANUP) {
|
|
1347
|
-
if (!currentProxyTool) {
|
|
1348
|
-
state.proxyDaemonMessage = { type: 'warning', msg: '⚠ Current tool has no persisted proxy config to clean', ts: Date.now() }
|
|
1349
|
-
return
|
|
1350
|
-
}
|
|
1351
|
-
const result = cleanupToolConfig(currentProxyTool)
|
|
1352
|
-
const { getToolMeta } = await import('./tool-metadata.js')
|
|
1353
|
-
const toolLabel = getToolMeta(currentProxyTool).label
|
|
1354
|
-
if (result.success) {
|
|
1355
|
-
state.proxyDaemonMessage = { type: 'success', msg: `✅ ${toolLabel} proxy config cleaned — all fcm-* entries removed`, ts: Date.now() }
|
|
1356
|
-
} else {
|
|
1357
|
-
state.proxyDaemonMessage = { type: 'error', msg: `❌ Cleanup failed: ${result.error}`, ts: Date.now() }
|
|
1358
|
-
}
|
|
1359
|
-
return
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
// 📖 Install / Uninstall daemon
|
|
1363
|
-
if (state.proxyDaemonCursor === ROW_DAEMON_INSTALL) {
|
|
1364
|
-
const { getDaemonStatus: _gds, installDaemon, uninstallDaemon, getPlatformSupport } = await import('./daemon-manager.js')
|
|
1365
|
-
const platform = getPlatformSupport()
|
|
1366
|
-
if (!platform.supported) {
|
|
1367
|
-
state.proxyDaemonMessage = { type: 'warning', msg: `⚠ ${platform.reason}`, ts: Date.now() }
|
|
1368
|
-
return
|
|
1369
|
-
}
|
|
1370
|
-
const current = await _gds()
|
|
1371
|
-
if (current.status === 'not-installed') {
|
|
1372
|
-
// 📖 Install daemon
|
|
1373
|
-
if (!proxySettings.enabled) {
|
|
1374
|
-
state.config.settings.proxy.enabled = true
|
|
1375
|
-
}
|
|
1376
|
-
state.config.settings.proxy.daemonEnabled = true
|
|
1377
|
-
state.config.settings.proxy.daemonConsent = new Date().toISOString()
|
|
1378
|
-
if (!state.config.settings.proxy.preferredPort || state.config.settings.proxy.preferredPort === 0) {
|
|
1379
|
-
state.config.settings.proxy.preferredPort = 18045
|
|
1380
|
-
}
|
|
1381
|
-
saveConfig(state.config)
|
|
1382
|
-
const result = installDaemon()
|
|
1383
|
-
if (result.success) {
|
|
1384
|
-
state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 background service installed and started!', ts: Date.now() }
|
|
1385
|
-
const ns = await _gds()
|
|
1386
|
-
state.daemonStatus = ns.status
|
|
1387
|
-
state.daemonInfo = ns.info || null
|
|
1388
|
-
} else {
|
|
1389
|
-
state.proxyDaemonMessage = { type: 'error', msg: `❌ Install failed: ${result.error}`, ts: Date.now() }
|
|
1390
|
-
}
|
|
1391
|
-
} else {
|
|
1392
|
-
// 📖 Uninstall daemon
|
|
1393
|
-
const result = uninstallDaemon()
|
|
1394
|
-
state.config.settings.proxy.daemonEnabled = false
|
|
1395
|
-
saveConfig(state.config)
|
|
1396
|
-
if (result.success) {
|
|
1397
|
-
state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 background service uninstalled.', ts: Date.now() }
|
|
1398
|
-
state.daemonStatus = 'not-installed'
|
|
1399
|
-
state.daemonInfo = null
|
|
1400
|
-
} else {
|
|
1401
|
-
state.proxyDaemonMessage = { type: 'error', msg: `❌ Uninstall failed: ${result.error}`, ts: Date.now() }
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
return
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
// 📖 Restart daemon
|
|
1408
|
-
if (state.proxyDaemonCursor === ROW_DAEMON_RESTART) {
|
|
1409
|
-
const { restartDaemon, getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1410
|
-
const result = restartDaemon()
|
|
1411
|
-
if (result.success) {
|
|
1412
|
-
state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 service restarted.', ts: Date.now() }
|
|
1413
|
-
// 📖 Wait a bit for the daemon to start up
|
|
1414
|
-
setTimeout(async () => {
|
|
1415
|
-
try {
|
|
1416
|
-
const ns = await _gds()
|
|
1417
|
-
state.daemonStatus = ns.status
|
|
1418
|
-
state.daemonInfo = ns.info || null
|
|
1419
|
-
} catch { /* ignore */ }
|
|
1420
|
-
}, 2000)
|
|
1421
|
-
} else {
|
|
1422
|
-
state.proxyDaemonMessage = { type: 'error', msg: `❌ Restart failed: ${result.error}`, ts: Date.now() }
|
|
1423
|
-
}
|
|
1424
|
-
return
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
// 📖 Stop daemon (SIGTERM)
|
|
1428
|
-
if (state.proxyDaemonCursor === ROW_DAEMON_STOP) {
|
|
1429
|
-
const { stopDaemon, getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1430
|
-
const result = stopDaemon()
|
|
1431
|
-
if (result.success) {
|
|
1432
|
-
const warning = result.willRestart ? ' (service may auto-restart it)' : ''
|
|
1433
|
-
state.proxyDaemonMessage = { type: 'success', msg: `✅ FCM Proxy V2 service stopped.${warning}`, ts: Date.now() }
|
|
1434
|
-
setTimeout(async () => {
|
|
1435
|
-
try {
|
|
1436
|
-
const ns = await _gds()
|
|
1437
|
-
state.daemonStatus = ns.status
|
|
1438
|
-
state.daemonInfo = ns.info || null
|
|
1439
|
-
} catch { /* ignore */ }
|
|
1440
|
-
}, 1500)
|
|
1441
|
-
} else {
|
|
1442
|
-
state.proxyDaemonMessage = { type: 'error', msg: `❌ Stop failed: ${result.error}`, ts: Date.now() }
|
|
1443
|
-
}
|
|
1444
|
-
return
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
// 📖 Force kill daemon (SIGKILL) — emergency
|
|
1448
|
-
if (state.proxyDaemonCursor === ROW_DAEMON_KILL) {
|
|
1449
|
-
const { killDaemonProcess, getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1450
|
-
const result = killDaemonProcess()
|
|
1451
|
-
if (result.success) {
|
|
1452
|
-
state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 service force-killed (SIGKILL).', ts: Date.now() }
|
|
1453
|
-
const ns = await _gds()
|
|
1454
|
-
state.daemonStatus = ns.status
|
|
1455
|
-
state.daemonInfo = ns.info || null
|
|
1456
|
-
} else {
|
|
1457
|
-
state.proxyDaemonMessage = { type: 'error', msg: `❌ Kill failed: ${result.error}`, ts: Date.now() }
|
|
1458
|
-
}
|
|
1459
|
-
return
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
// 📖 View daemon logs
|
|
1463
|
-
if (state.proxyDaemonCursor === ROW_DAEMON_LOGS) {
|
|
1464
|
-
const { getDaemonLogPath } = await import('./daemon-manager.js')
|
|
1465
|
-
const logPath = getDaemonLogPath()
|
|
1466
|
-
try {
|
|
1467
|
-
const { readFileSync, existsSync } = await import('node:fs')
|
|
1468
|
-
if (!existsSync(logPath)) {
|
|
1469
|
-
state.proxyDaemonMessage = { type: 'warning', msg: `⚠ No log file found at ${logPath}`, ts: Date.now() }
|
|
1470
|
-
return
|
|
1471
|
-
}
|
|
1472
|
-
const content = readFileSync(logPath, 'utf8')
|
|
1473
|
-
const logLines = content.split('\n')
|
|
1474
|
-
const last50 = logLines.slice(-50).join('\n')
|
|
1475
|
-
// 📖 Display in the log overlay (repurpose log view)
|
|
1476
|
-
state.proxyDaemonOpen = false
|
|
1477
|
-
state.logVisible = true
|
|
1478
|
-
state.logScrollOffset = 0
|
|
1479
|
-
state._daemonLogContent = last50
|
|
1480
|
-
state.proxyDaemonMessage = { type: 'success', msg: `📖 Showing last ${Math.min(50, logLines.length)} lines from ${logPath}`, ts: Date.now() }
|
|
1481
|
-
} catch (err) {
|
|
1482
|
-
state.proxyDaemonMessage = { type: 'error', msg: `❌ Could not read logs: ${err.message}`, ts: Date.now() }
|
|
1483
|
-
}
|
|
1484
|
-
return
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
return // 📖 Swallow all other keys while proxy/daemon overlay is open
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
1180
|
// 📖 P key: open settings screen
|
|
1492
1181
|
if (key.name === 'p' && !key.shift) {
|
|
1493
1182
|
state.settingsOpen = true
|
|
1494
1183
|
state.settingsCursor = 0
|
|
1495
1184
|
state.settingsEditMode = false
|
|
1496
1185
|
state.settingsAddKeyMode = false
|
|
1497
|
-
state.settingsProxyPortEditMode = false
|
|
1498
|
-
state.settingsProxyPortBuffer = ''
|
|
1499
1186
|
state.settingsEditBuffer = ''
|
|
1500
1187
|
state.settingsScrollOffset = 0
|
|
1501
|
-
// 📖 Refresh daemon status when opening settings
|
|
1502
|
-
import('./daemon-manager.js').then(dm => {
|
|
1503
|
-
dm.getDaemonStatus().then(s => {
|
|
1504
|
-
state.daemonStatus = s.status
|
|
1505
|
-
state.daemonInfo = s.info || null
|
|
1506
|
-
}).catch(() => {})
|
|
1507
|
-
}).catch(() => {})
|
|
1508
1188
|
return
|
|
1509
1189
|
}
|
|
1510
1190
|
|
|
@@ -1624,22 +1304,6 @@ const updateRowIdx = providerKeys.length
|
|
|
1624
1304
|
return
|
|
1625
1305
|
}
|
|
1626
1306
|
|
|
1627
|
-
// 📖 J key: open FCM Proxy V2 settings overlay directly (bypasses Settings screen)
|
|
1628
|
-
if (key.name === 'j') {
|
|
1629
|
-
state.proxyDaemonOpen = true
|
|
1630
|
-
state.proxyDaemonCursor = 0
|
|
1631
|
-
state.proxyDaemonScrollOffset = 0
|
|
1632
|
-
state.proxyDaemonMessage = null
|
|
1633
|
-
// 📖 Refresh daemon status when entering
|
|
1634
|
-
try {
|
|
1635
|
-
const { getDaemonStatus: _gds } = await import('./daemon-manager.js')
|
|
1636
|
-
const st = await _gds()
|
|
1637
|
-
state.daemonStatus = st.status
|
|
1638
|
-
state.daemonInfo = st.info || null
|
|
1639
|
-
} catch { /* ignore */ }
|
|
1640
|
-
return
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
1307
|
// 📖 I key: open Feedback overlay (anonymous Discord feedback)
|
|
1644
1308
|
if (key.name === 'i') {
|
|
1645
1309
|
state.feedbackOpen = true
|
|
@@ -1730,14 +1394,6 @@ const updateRowIdx = providerKeys.length
|
|
|
1730
1394
|
return
|
|
1731
1395
|
}
|
|
1732
1396
|
|
|
1733
|
-
// 📖 X key: toggle the log page overlay (shows recent requests from request-log.jsonl).
|
|
1734
|
-
// 📖 NOTE: X was previously used for ping-interval increase; that binding moved to '='.
|
|
1735
|
-
if (key.name === 'x') {
|
|
1736
|
-
state.logVisible = !state.logVisible
|
|
1737
|
-
if (state.logVisible) state.logScrollOffset = 0
|
|
1738
|
-
return
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
1397
|
if (key.name === 'up') {
|
|
1742
1398
|
// 📖 Main list wrap navigation: top -> bottom on Up.
|
|
1743
1399
|
const count = state.visibleSorted.length
|
|
@@ -1802,20 +1458,11 @@ const updateRowIdx = providerKeys.length
|
|
|
1802
1458
|
|
|
1803
1459
|
// 📖 Dispatch to the correct integration based on active mode
|
|
1804
1460
|
if (state.mode === 'openclaw') {
|
|
1805
|
-
await startOpenClaw(userSelected,
|
|
1461
|
+
await startOpenClaw(userSelected, state.config)
|
|
1806
1462
|
} else if (state.mode === 'opencode-desktop') {
|
|
1807
1463
|
await startOpenCodeDesktop(userSelected, state.config)
|
|
1808
1464
|
} else if (state.mode === 'opencode') {
|
|
1809
|
-
|
|
1810
|
-
if (isProxyEnabledForConfig(state.config) && topology.accounts.length > 0) {
|
|
1811
|
-
await startProxyAndLaunch(userSelected, state.config)
|
|
1812
|
-
} else {
|
|
1813
|
-
if (isProxyEnabledForConfig(state.config) && topology.accounts.length === 0) {
|
|
1814
|
-
console.log(chalk.yellow(' Proxy mode is enabled, but no proxy-capable API keys were found. Falling back to direct flow.'))
|
|
1815
|
-
console.log()
|
|
1816
|
-
}
|
|
1817
|
-
await startOpenCode(userSelected, state.config)
|
|
1818
|
-
}
|
|
1465
|
+
await startOpenCode(userSelected, state.config)
|
|
1819
1466
|
} else {
|
|
1820
1467
|
await startExternalTool(state.mode, userSelected, state.config)
|
|
1821
1468
|
}
|