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.
@@ -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,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 === 'connection') {
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.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
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
- const widthWarningRowIdx = updateRowIdx + 1
921
- const proxyDaemonRowIdx = widthWarningRowIdx + 1
922
- const changelogViewRowIdx = proxyDaemonRowIdx + 1
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
- const maxRowIdx = changelogViewRowIdx
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
- // 📖 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 */ }
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 (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyDaemonRowIdx || state.settingsCursor === changelogViewRowIdx) return
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 (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyDaemonRowIdx || state.settingsCursor === changelogViewRowIdx) return
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, apiKey)
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
- 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
- }
1437
+ await startOpenCode(userSelected, state.config)
1819
1438
  } else {
1820
1439
  await startExternalTool(state.mode, userSelected, state.config)
1821
1440
  }