free-coding-models 0.3.11 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +112 -1134
- package/bin/free-coding-models.js +13 -167
- package/package.json +2 -3
- package/src/cli-help.js +0 -18
- package/src/config.js +5 -117
- package/src/endpoint-installer.js +26 -64
- package/src/key-handler.js +56 -437
- package/src/legacy-proxy-cleanup.js +432 -0
- package/src/openclaw.js +69 -108
- package/src/opencode-config.js +48 -0
- package/src/opencode.js +6 -248
- package/src/overlays.js +23 -517
- package/src/product-flags.js +14 -0
- package/src/render-helpers.js +2 -34
- package/src/render-table.js +10 -18
- package/src/testfcm.js +90 -43
- package/src/token-usage-reader.js +9 -38
- package/src/tool-launchers.js +235 -409
- package/src/tool-metadata.js +0 -7
- package/src/utils.js +3 -68
- package/bin/fcm-proxy-daemon.js +0 -242
- package/src/account-manager.js +0 -634
- package/src/anthropic-translator.js +0 -440
- package/src/daemon-manager.js +0 -527
- package/src/error-classifier.js +0 -157
- package/src/log-reader.js +0 -195
- package/src/opencode-sync.js +0 -200
- package/src/proxy-foreground.js +0 -234
- package/src/proxy-server.js +0 -1506
- package/src/proxy-sync.js +0 -591
- package/src/proxy-topology.js +0 -85
- package/src/request-transformer.js +0 -180
- package/src/responses-translator.js +0 -423
- package/src/token-stats.js +0 -320
package/src/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,39 @@ 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
|
+
|
|
332
353
|
function resetInstallEndpointsOverlay() {
|
|
333
354
|
state.installEndpointsOpen = false
|
|
334
355
|
state.installEndpointsPhase = 'providers'
|
|
@@ -385,7 +406,6 @@ export function createKeyHandler(ctx) {
|
|
|
385
406
|
|
|
386
407
|
const providerChoices = getConfiguredInstallableProviders(state.config)
|
|
387
408
|
const toolChoices = getInstallTargetModes()
|
|
388
|
-
const connectionChoices = CONNECTION_MODES || []
|
|
389
409
|
const modelChoices = state.installEndpointsProviderKey
|
|
390
410
|
? getProviderCatalogModels(state.installEndpointsProviderKey)
|
|
391
411
|
: []
|
|
@@ -394,7 +414,6 @@ export function createKeyHandler(ctx) {
|
|
|
394
414
|
const maxIndexByPhase = () => {
|
|
395
415
|
if (state.installEndpointsPhase === 'providers') return Math.max(0, providerChoices.length - 1)
|
|
396
416
|
if (state.installEndpointsPhase === 'tools') return Math.max(0, toolChoices.length - 1)
|
|
397
|
-
if (state.installEndpointsPhase === 'connection') return Math.max(0, connectionChoices.length - 1)
|
|
398
417
|
if (state.installEndpointsPhase === 'scope') return 1
|
|
399
418
|
if (state.installEndpointsPhase === 'models') return Math.max(0, modelChoices.length - 1)
|
|
400
419
|
return 0
|
|
@@ -437,18 +456,12 @@ export function createKeyHandler(ctx) {
|
|
|
437
456
|
state.installEndpointsScrollOffset = 0
|
|
438
457
|
return
|
|
439
458
|
}
|
|
440
|
-
if (state.installEndpointsPhase === '
|
|
459
|
+
if (state.installEndpointsPhase === 'scope') {
|
|
441
460
|
state.installEndpointsPhase = 'tools'
|
|
442
461
|
state.installEndpointsCursor = 0
|
|
443
462
|
state.installEndpointsScrollOffset = 0
|
|
444
463
|
return
|
|
445
464
|
}
|
|
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
465
|
if (state.installEndpointsPhase === 'models') {
|
|
453
466
|
state.installEndpointsPhase = 'scope'
|
|
454
467
|
state.installEndpointsCursor = state.installEndpointsScope === 'selected' ? 1 : 0
|
|
@@ -481,20 +494,7 @@ export function createKeyHandler(ctx) {
|
|
|
481
494
|
const selectedToolMode = toolChoices[state.installEndpointsCursor]
|
|
482
495
|
if (!selectedToolMode) return
|
|
483
496
|
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
|
|
497
|
+
state.installEndpointsConnectionMode = 'direct'
|
|
498
498
|
state.installEndpointsPhase = 'scope'
|
|
499
499
|
state.installEndpointsCursor = 0
|
|
500
500
|
state.installEndpointsScrollOffset = 0
|
|
@@ -665,29 +665,6 @@ export function createKeyHandler(ctx) {
|
|
|
665
665
|
return
|
|
666
666
|
}
|
|
667
667
|
|
|
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
668
|
// 📖 Changelog overlay: two-phase (index + details) with keyboard navigation
|
|
692
669
|
if (state.changelogOpen) {
|
|
693
670
|
const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
|
|
@@ -914,14 +891,13 @@ export function createKeyHandler(ctx) {
|
|
|
914
891
|
|
|
915
892
|
// ─── Settings overlay keyboard handling ───────────────────────────────────
|
|
916
893
|
if (state.settingsOpen) {
|
|
917
|
-
const proxySettings = getProxySettings(state.config)
|
|
918
894
|
const providerKeys = Object.keys(sources)
|
|
919
|
-
const updateRowIdx = providerKeys.length
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
895
|
+
const updateRowIdx = providerKeys.length
|
|
896
|
+
const widthWarningRowIdx = updateRowIdx + 1
|
|
897
|
+
const cleanupLegacyProxyRowIdx = widthWarningRowIdx + 1
|
|
898
|
+
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
923
899
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
924
|
-
|
|
900
|
+
const maxRowIdx = changelogViewRowIdx
|
|
925
901
|
|
|
926
902
|
// 📖 Edit/Add-key mode: capture typed characters for the API key
|
|
927
903
|
if (state.settingsEditMode || state.settingsAddKeyMode) {
|
|
@@ -982,8 +958,6 @@ const updateRowIdx = providerKeys.length
|
|
|
982
958
|
state.settingsOpen = false
|
|
983
959
|
state.settingsEditMode = false
|
|
984
960
|
state.settingsAddKeyMode = false
|
|
985
|
-
state.settingsProxyPortEditMode = false
|
|
986
|
-
state.settingsProxyPortBuffer = ''
|
|
987
961
|
state.settingsEditBuffer = ''
|
|
988
962
|
state.settingsSyncStatus = null // 📖 Clear sync status on close
|
|
989
963
|
// 📖 Rebuild results: add models from newly enabled providers, remove disabled
|
|
@@ -1072,20 +1046,8 @@ const updateRowIdx = providerKeys.length
|
|
|
1072
1046
|
return
|
|
1073
1047
|
}
|
|
1074
1048
|
|
|
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 */ }
|
|
1049
|
+
if (state.settingsCursor === cleanupLegacyProxyRowIdx) {
|
|
1050
|
+
runLegacyProxyCleanup()
|
|
1089
1051
|
return
|
|
1090
1052
|
}
|
|
1091
1053
|
|
|
@@ -1111,7 +1073,11 @@ const updateRowIdx = providerKeys.length
|
|
|
1111
1073
|
|
|
1112
1074
|
if (key.name === 'space') {
|
|
1113
1075
|
// 📖 Exclude certain rows from space toggle
|
|
1114
|
-
if (
|
|
1076
|
+
if (
|
|
1077
|
+
state.settingsCursor === updateRowIdx
|
|
1078
|
+
|| state.settingsCursor === cleanupLegacyProxyRowIdx
|
|
1079
|
+
|| state.settingsCursor === changelogViewRowIdx
|
|
1080
|
+
) return
|
|
1115
1081
|
// 📖 Widths Warning toggle (disable/enable)
|
|
1116
1082
|
if (state.settingsCursor === widthWarningRowIdx) {
|
|
1117
1083
|
if (!state.config.settings) state.config.settings = {}
|
|
@@ -1131,7 +1097,11 @@ const updateRowIdx = providerKeys.length
|
|
|
1131
1097
|
}
|
|
1132
1098
|
|
|
1133
1099
|
if (key.name === 't') {
|
|
1134
|
-
if (
|
|
1100
|
+
if (
|
|
1101
|
+
state.settingsCursor === updateRowIdx
|
|
1102
|
+
|| state.settingsCursor === cleanupLegacyProxyRowIdx
|
|
1103
|
+
|| state.settingsCursor === changelogViewRowIdx
|
|
1104
|
+
) return
|
|
1135
1105
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1136
1106
|
|
|
1137
1107
|
// 📖 Test the selected provider's key (fires a real ping)
|
|
@@ -1149,47 +1119,6 @@ const updateRowIdx = providerKeys.length
|
|
|
1149
1119
|
|
|
1150
1120
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1151
1121
|
|
|
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
1122
|
// 📖 + key: open add-key input (empty buffer) — appends new key on Enter
|
|
1194
1123
|
if ((str === '+' || key.name === '+') && state.settingsCursor < providerKeys.length) {
|
|
1195
1124
|
state.settingsEditBuffer = '' // 📖 Start with empty buffer (not existing key)
|
|
@@ -1220,291 +1149,14 @@ const updateRowIdx = providerKeys.length
|
|
|
1220
1149
|
return // 📖 Swallow all other keys while settings is open
|
|
1221
1150
|
}
|
|
1222
1151
|
|
|
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
1152
|
// 📖 P key: open settings screen
|
|
1492
1153
|
if (key.name === 'p' && !key.shift) {
|
|
1493
1154
|
state.settingsOpen = true
|
|
1494
1155
|
state.settingsCursor = 0
|
|
1495
1156
|
state.settingsEditMode = false
|
|
1496
1157
|
state.settingsAddKeyMode = false
|
|
1497
|
-
state.settingsProxyPortEditMode = false
|
|
1498
|
-
state.settingsProxyPortBuffer = ''
|
|
1499
1158
|
state.settingsEditBuffer = ''
|
|
1500
1159
|
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
1160
|
return
|
|
1509
1161
|
}
|
|
1510
1162
|
|
|
@@ -1624,22 +1276,6 @@ const updateRowIdx = providerKeys.length
|
|
|
1624
1276
|
return
|
|
1625
1277
|
}
|
|
1626
1278
|
|
|
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
1279
|
// 📖 I key: open Feedback overlay (anonymous Discord feedback)
|
|
1644
1280
|
if (key.name === 'i') {
|
|
1645
1281
|
state.feedbackOpen = true
|
|
@@ -1730,14 +1366,6 @@ const updateRowIdx = providerKeys.length
|
|
|
1730
1366
|
return
|
|
1731
1367
|
}
|
|
1732
1368
|
|
|
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
1369
|
if (key.name === 'up') {
|
|
1742
1370
|
// 📖 Main list wrap navigation: top -> bottom on Up.
|
|
1743
1371
|
const count = state.visibleSorted.length
|
|
@@ -1802,20 +1430,11 @@ const updateRowIdx = providerKeys.length
|
|
|
1802
1430
|
|
|
1803
1431
|
// 📖 Dispatch to the correct integration based on active mode
|
|
1804
1432
|
if (state.mode === 'openclaw') {
|
|
1805
|
-
await startOpenClaw(userSelected,
|
|
1433
|
+
await startOpenClaw(userSelected, state.config)
|
|
1806
1434
|
} else if (state.mode === 'opencode-desktop') {
|
|
1807
1435
|
await startOpenCodeDesktop(userSelected, state.config)
|
|
1808
1436
|
} 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
|
-
}
|
|
1437
|
+
await startOpenCode(userSelected, state.config)
|
|
1819
1438
|
} else {
|
|
1820
1439
|
await startExternalTool(state.mode, userSelected, state.config)
|
|
1821
1440
|
}
|