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.
@@ -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 { resolveProxySyncToolMode } from './proxy-sync.js'
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 === 'connection') {
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.installEndpointsPhase = 'connection'
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
- const widthWarningRowIdx = updateRowIdx + 1
921
- const proxyDaemonRowIdx = widthWarningRowIdx + 1
922
- const changelogViewRowIdx = proxyDaemonRowIdx + 1
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
- const maxRowIdx = changelogViewRowIdx
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
- if (!state.config.settings) state.config.settings = {}
1070
- state.config.settings.disableWidthsWarning = !state.config.settings.disableWidthsWarning
1071
- saveConfig(state.config)
1075
+ toggleWidthsWarningSetting()
1072
1076
  return
1073
1077
  }
1074
1078
 
1075
- // 📖 Proxy & Daemon row: Enter → open dedicated overlay
1076
- if (state.settingsCursor === proxyDaemonRowIdx) {
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 (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyDaemonRowIdx || state.settingsCursor === changelogViewRowIdx) return
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
- if (!state.config.settings) state.config.settings = {}
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 (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyDaemonRowIdx || state.settingsCursor === changelogViewRowIdx) return
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, apiKey)
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
- const topology = buildProxyTopologyFromConfig(state.config)
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
  }