free-coding-models 0.1.87 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -45
- package/bin/free-coding-models.js +65 -14
- package/package.json +1 -1
- package/src/config.js +42 -4
- package/src/key-handler.js +105 -13
- package/src/opencode-sync.js +41 -0
- package/src/opencode.js +23 -2
- package/src/overlays.js +74 -13
- package/src/render-table.js +72 -16
- package/src/token-usage-reader.js +5 -5
- package/src/tool-launchers.js +319 -0
- package/src/tool-metadata.js +63 -0
- package/src/updater.js +128 -30
- package/src/utils.js +40 -3
package/src/key-handler.js
CHANGED
|
@@ -104,6 +104,7 @@ export function createKeyHandler(ctx) {
|
|
|
104
104
|
MODELS,
|
|
105
105
|
sources,
|
|
106
106
|
getApiKey,
|
|
107
|
+
getProxySettings,
|
|
107
108
|
resolveApiKeys,
|
|
108
109
|
addApiKey,
|
|
109
110
|
removeApiKey,
|
|
@@ -125,6 +126,7 @@ export function createKeyHandler(ctx) {
|
|
|
125
126
|
ENV_VAR_NAMES,
|
|
126
127
|
ensureProxyRunning,
|
|
127
128
|
syncToOpenCode,
|
|
129
|
+
cleanupOpenCodeProxyConfig,
|
|
128
130
|
restoreOpenCodeBackup,
|
|
129
131
|
checkForUpdateDetailed,
|
|
130
132
|
runUpdate,
|
|
@@ -132,7 +134,10 @@ export function createKeyHandler(ctx) {
|
|
|
132
134
|
startOpenCodeDesktop,
|
|
133
135
|
startOpenCode,
|
|
134
136
|
startProxyAndLaunch,
|
|
137
|
+
startExternalTool,
|
|
135
138
|
buildProxyTopologyFromConfig,
|
|
139
|
+
isProxyEnabledForConfig,
|
|
140
|
+
getToolModeOrder,
|
|
136
141
|
startRecommendAnalysis,
|
|
137
142
|
stopRecommendAnalysis,
|
|
138
143
|
sendFeatureRequest,
|
|
@@ -261,6 +266,7 @@ export function createKeyHandler(ctx) {
|
|
|
261
266
|
sortAsc: state.sortDirection === 'asc',
|
|
262
267
|
pingInterval: state.pingInterval,
|
|
263
268
|
hideUnconfiguredModels: state.hideUnconfiguredModels,
|
|
269
|
+
proxy: getProxySettings(state.config),
|
|
264
270
|
})
|
|
265
271
|
setActiveProfile(state.config, name)
|
|
266
272
|
state.activeProfile = name
|
|
@@ -558,12 +564,17 @@ export function createKeyHandler(ctx) {
|
|
|
558
564
|
|
|
559
565
|
// ─── Settings overlay keyboard handling ───────────────────────────────────
|
|
560
566
|
if (state.settingsOpen) {
|
|
567
|
+
const proxySettings = getProxySettings(state.config)
|
|
561
568
|
const providerKeys = Object.keys(sources)
|
|
562
569
|
const updateRowIdx = providerKeys.length
|
|
563
|
-
|
|
570
|
+
const proxyEnabledRowIdx = updateRowIdx + 1
|
|
571
|
+
const proxySyncRowIdx = updateRowIdx + 2
|
|
572
|
+
const proxyPortRowIdx = updateRowIdx + 3
|
|
573
|
+
const proxyCleanupRowIdx = updateRowIdx + 4
|
|
574
|
+
// 📖 Profile rows start after maintenance + proxy rows — one row per saved profile
|
|
564
575
|
const savedProfiles = listProfiles(state.config)
|
|
565
|
-
const profileStartIdx = updateRowIdx +
|
|
566
|
-
const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 :
|
|
576
|
+
const profileStartIdx = updateRowIdx + 5
|
|
577
|
+
const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : proxyCleanupRowIdx
|
|
567
578
|
|
|
568
579
|
// 📖 Edit/Add-key mode: capture typed characters for the API key
|
|
569
580
|
if (state.settingsEditMode || state.settingsAddKeyMode) {
|
|
@@ -608,12 +619,40 @@ export function createKeyHandler(ctx) {
|
|
|
608
619
|
return
|
|
609
620
|
}
|
|
610
621
|
|
|
622
|
+
// 📖 Dedicated inline editor for the preferred proxy port. 0 = OS auto-port.
|
|
623
|
+
if (state.settingsProxyPortEditMode) {
|
|
624
|
+
if (key.name === 'return') {
|
|
625
|
+
const raw = state.settingsProxyPortBuffer.trim()
|
|
626
|
+
const parsed = raw === '' ? 0 : Number.parseInt(raw, 10)
|
|
627
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
628
|
+
state.settingsSyncStatus = { type: 'error', msg: '❌ Proxy port must be 0 (auto) or a number between 1 and 65535' }
|
|
629
|
+
return
|
|
630
|
+
}
|
|
631
|
+
if (!state.config.settings) state.config.settings = {}
|
|
632
|
+
state.config.settings.proxy = { ...proxySettings, preferredPort: parsed }
|
|
633
|
+
saveConfig(state.config)
|
|
634
|
+
state.settingsProxyPortEditMode = false
|
|
635
|
+
state.settingsProxyPortBuffer = ''
|
|
636
|
+
state.settingsSyncStatus = { type: 'success', msg: `✅ Preferred proxy port saved: ${parsed === 0 ? 'auto' : parsed}` }
|
|
637
|
+
} else if (key.name === 'escape') {
|
|
638
|
+
state.settingsProxyPortEditMode = false
|
|
639
|
+
state.settingsProxyPortBuffer = ''
|
|
640
|
+
} else if (key.name === 'backspace') {
|
|
641
|
+
state.settingsProxyPortBuffer = state.settingsProxyPortBuffer.slice(0, -1)
|
|
642
|
+
} else if (str && /^[0-9]$/.test(str) && state.settingsProxyPortBuffer.length < 5) {
|
|
643
|
+
state.settingsProxyPortBuffer += str
|
|
644
|
+
}
|
|
645
|
+
return
|
|
646
|
+
}
|
|
647
|
+
|
|
611
648
|
// 📖 Normal settings navigation
|
|
612
649
|
if (key.name === 'escape' || key.name === 'p') {
|
|
613
650
|
// 📖 Close settings — rebuild results to reflect provider changes
|
|
614
651
|
state.settingsOpen = false
|
|
615
652
|
state.settingsEditMode = false
|
|
616
653
|
state.settingsAddKeyMode = false
|
|
654
|
+
state.settingsProxyPortEditMode = false
|
|
655
|
+
state.settingsProxyPortBuffer = ''
|
|
617
656
|
state.settingsEditBuffer = ''
|
|
618
657
|
state.settingsSyncStatus = null // 📖 Clear sync status on close
|
|
619
658
|
// 📖 Rebuild results: add models from newly enabled providers, remove disabled
|
|
@@ -694,6 +733,21 @@ export function createKeyHandler(ctx) {
|
|
|
694
733
|
return
|
|
695
734
|
}
|
|
696
735
|
|
|
736
|
+
if (state.settingsCursor === proxyPortRowIdx) {
|
|
737
|
+
state.settingsProxyPortEditMode = true
|
|
738
|
+
state.settingsProxyPortBuffer = String(proxySettings.preferredPort || 0)
|
|
739
|
+
return
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (state.settingsCursor === proxyCleanupRowIdx) {
|
|
743
|
+
const cleaned = cleanupOpenCodeProxyConfig()
|
|
744
|
+
state.settingsSyncStatus = {
|
|
745
|
+
type: 'success',
|
|
746
|
+
msg: `✅ Proxy cleanup done (${cleaned.removedProvider ? 'provider removed' : 'no provider found'}, ${cleaned.removedModel ? 'default model cleared' : 'default model unchanged'})`,
|
|
747
|
+
}
|
|
748
|
+
return
|
|
749
|
+
}
|
|
750
|
+
|
|
697
751
|
// 📖 Profile row: Enter → load the selected profile (apply its settings live)
|
|
698
752
|
if (state.settingsCursor >= profileStartIdx && savedProfiles.length > 0) {
|
|
699
753
|
const profileIdx = state.settingsCursor - profileStartIdx
|
|
@@ -729,10 +783,27 @@ export function createKeyHandler(ctx) {
|
|
|
729
783
|
}
|
|
730
784
|
|
|
731
785
|
if (key.name === 'space') {
|
|
732
|
-
if (state.settingsCursor === updateRowIdx) return
|
|
786
|
+
if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyPortRowIdx || state.settingsCursor === proxyCleanupRowIdx) return
|
|
733
787
|
// 📖 Profile rows don't respond to Space
|
|
734
788
|
if (state.settingsCursor >= profileStartIdx) return
|
|
735
789
|
|
|
790
|
+
if (state.settingsCursor === proxyEnabledRowIdx || state.settingsCursor === proxySyncRowIdx) {
|
|
791
|
+
if (!state.config.settings) state.config.settings = {}
|
|
792
|
+
state.config.settings.proxy = {
|
|
793
|
+
...proxySettings,
|
|
794
|
+
enabled: state.settingsCursor === proxyEnabledRowIdx ? !proxySettings.enabled : proxySettings.enabled,
|
|
795
|
+
syncToOpenCode: state.settingsCursor === proxySyncRowIdx ? !proxySettings.syncToOpenCode : proxySettings.syncToOpenCode,
|
|
796
|
+
}
|
|
797
|
+
saveConfig(state.config)
|
|
798
|
+
state.settingsSyncStatus = {
|
|
799
|
+
type: 'success',
|
|
800
|
+
msg: state.settingsCursor === proxyEnabledRowIdx
|
|
801
|
+
? `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`
|
|
802
|
+
: `✅ OpenCode proxy sync ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`,
|
|
803
|
+
}
|
|
804
|
+
return
|
|
805
|
+
}
|
|
806
|
+
|
|
736
807
|
// 📖 Toggle enabled/disabled for selected provider
|
|
737
808
|
const pk = providerKeys[state.settingsCursor]
|
|
738
809
|
if (!state.config.providers) state.config.providers = {}
|
|
@@ -785,6 +856,14 @@ export function createKeyHandler(ctx) {
|
|
|
785
856
|
// 📖 S key: sync FCM provider entries to OpenCode config (merge, don't replace)
|
|
786
857
|
if (key.name === 's' && !key.shift && !key.ctrl) {
|
|
787
858
|
try {
|
|
859
|
+
if (!proxySettings.enabled) {
|
|
860
|
+
state.settingsSyncStatus = { type: 'error', msg: '⚠ Enable Proxy mode first if you want to sync fcm-proxy into OpenCode' }
|
|
861
|
+
return
|
|
862
|
+
}
|
|
863
|
+
if (!proxySettings.syncToOpenCode) {
|
|
864
|
+
state.settingsSyncStatus = { type: 'error', msg: '⚠ Enable "Persist proxy in OpenCode" first, or use the direct OpenCode flow only' }
|
|
865
|
+
return
|
|
866
|
+
}
|
|
788
867
|
// 📖 Sync now also ensures proxy is running, so OpenCode can use fcm-proxy immediately.
|
|
789
868
|
const started = await ensureProxyRunning(state.config)
|
|
790
869
|
const result = syncToOpenCode(state.config, sources, mergedModels, {
|
|
@@ -847,6 +926,8 @@ export function createKeyHandler(ctx) {
|
|
|
847
926
|
state.settingsCursor = 0
|
|
848
927
|
state.settingsEditMode = false
|
|
849
928
|
state.settingsAddKeyMode = false
|
|
929
|
+
state.settingsProxyPortEditMode = false
|
|
930
|
+
state.settingsProxyPortBuffer = ''
|
|
850
931
|
state.settingsEditBuffer = ''
|
|
851
932
|
state.settingsScrollOffset = 0
|
|
852
933
|
return
|
|
@@ -875,6 +956,7 @@ export function createKeyHandler(ctx) {
|
|
|
875
956
|
sortAsc: state.sortDirection === 'asc',
|
|
876
957
|
pingInterval: state.pingInterval,
|
|
877
958
|
hideUnconfiguredModels: state.hideUnconfiguredModels,
|
|
959
|
+
proxy: getProxySettings(state.config),
|
|
878
960
|
})
|
|
879
961
|
setActiveProfile(state.config, 'default')
|
|
880
962
|
state.activeProfile = 'default'
|
|
@@ -1054,9 +1136,9 @@ export function createKeyHandler(ctx) {
|
|
|
1054
1136
|
return
|
|
1055
1137
|
}
|
|
1056
1138
|
|
|
1057
|
-
// 📖 Mode toggle key: Z
|
|
1139
|
+
// 📖 Mode toggle key: Z cycles through the supported tool targets.
|
|
1058
1140
|
if (key.name === 'z') {
|
|
1059
|
-
const modeOrder =
|
|
1141
|
+
const modeOrder = getToolModeOrder()
|
|
1060
1142
|
const currentIndex = modeOrder.indexOf(state.mode)
|
|
1061
1143
|
const nextIndex = (currentIndex + 1) % modeOrder.length
|
|
1062
1144
|
state.mode = modeOrder[nextIndex]
|
|
@@ -1094,6 +1176,12 @@ export function createKeyHandler(ctx) {
|
|
|
1094
1176
|
return
|
|
1095
1177
|
}
|
|
1096
1178
|
|
|
1179
|
+
// 📖 Esc can dismiss the narrow-terminal warning immediately without quitting the app.
|
|
1180
|
+
if (key.name === 'escape' && state.terminalCols > 0 && state.terminalCols < 166) {
|
|
1181
|
+
state.widthWarningDismissed = true
|
|
1182
|
+
return
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1097
1185
|
if (key.name === 'return') { // Enter
|
|
1098
1186
|
// 📖 Use the cached visible+sorted array — guaranteed to match what's on screen
|
|
1099
1187
|
const selected = state.visibleSorted[state.cursor]
|
|
@@ -1121,7 +1209,7 @@ export function createKeyHandler(ctx) {
|
|
|
1121
1209
|
const selectedApiKey = getApiKey(state.config, selected.providerKey)
|
|
1122
1210
|
if (!selectedApiKey) {
|
|
1123
1211
|
console.log(chalk.yellow(` Warning: No API key configured for ${selected.providerKey}.`))
|
|
1124
|
-
console.log(chalk.yellow(`
|
|
1212
|
+
console.log(chalk.yellow(` The selected tool may not be able to use ${selected.label}.`))
|
|
1125
1213
|
console.log(chalk.dim(` Set ${ENV_VAR_NAMES[selected.providerKey] || selected.providerKey.toUpperCase() + '_API_KEY'} or configure via settings (P key).`))
|
|
1126
1214
|
console.log()
|
|
1127
1215
|
}
|
|
@@ -1132,15 +1220,19 @@ export function createKeyHandler(ctx) {
|
|
|
1132
1220
|
await startOpenClaw(userSelected, apiKey)
|
|
1133
1221
|
} else if (state.mode === 'opencode-desktop') {
|
|
1134
1222
|
await startOpenCodeDesktop(userSelected, state.config)
|
|
1135
|
-
} else {
|
|
1223
|
+
} else if (state.mode === 'opencode') {
|
|
1136
1224
|
const topology = buildProxyTopologyFromConfig(state.config)
|
|
1137
|
-
if (topology.accounts.length
|
|
1138
|
-
console.log(chalk.yellow(' No API keys found for proxy model catalog. Falling back to direct flow.'))
|
|
1139
|
-
console.log()
|
|
1140
|
-
await startOpenCode(userSelected, state.config)
|
|
1141
|
-
} else {
|
|
1225
|
+
if (isProxyEnabledForConfig(state.config) && topology.accounts.length > 0) {
|
|
1142
1226
|
await startProxyAndLaunch(userSelected, state.config)
|
|
1227
|
+
} else {
|
|
1228
|
+
if (isProxyEnabledForConfig(state.config) && topology.accounts.length === 0) {
|
|
1229
|
+
console.log(chalk.yellow(' Proxy mode is enabled, but no proxy-capable API keys were found. Falling back to direct flow.'))
|
|
1230
|
+
console.log()
|
|
1231
|
+
}
|
|
1232
|
+
await startOpenCode(userSelected, state.config)
|
|
1143
1233
|
}
|
|
1234
|
+
} else {
|
|
1235
|
+
await startExternalTool(state.mode, userSelected, state.config)
|
|
1144
1236
|
}
|
|
1145
1237
|
process.exit(0)
|
|
1146
1238
|
}
|
package/src/opencode-sync.js
CHANGED
|
@@ -128,6 +128,30 @@ export function mergeOcConfig(ocConfig, mergedModels, proxyInfo = {}) {
|
|
|
128
128
|
return ocConfig
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Pure cleanup: remove only the persisted FCM proxy provider and any default
|
|
133
|
+
* model that still points at it. Other OpenCode providers stay untouched.
|
|
134
|
+
*
|
|
135
|
+
* @param {Object} ocConfig - Existing OpenCode config object (will be mutated in-place)
|
|
136
|
+
* @returns {{ removedProvider: boolean, removedModel: boolean, config: Object }}
|
|
137
|
+
*/
|
|
138
|
+
export function removeFcmProxyFromConfig(ocConfig) {
|
|
139
|
+
if (!ocConfig || typeof ocConfig !== 'object') {
|
|
140
|
+
return { removedProvider: false, removedModel: false, config: {} }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const hadProvider = Boolean(ocConfig.provider?.[FCM_PROVIDER_ID])
|
|
144
|
+
if (hadProvider) {
|
|
145
|
+
delete ocConfig.provider[FCM_PROVIDER_ID]
|
|
146
|
+
if (Object.keys(ocConfig.provider).length === 0) delete ocConfig.provider
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const hadModel = typeof ocConfig.model === 'string' && ocConfig.model.startsWith(`${FCM_PROVIDER_ID}/`)
|
|
150
|
+
if (hadModel) delete ocConfig.model
|
|
151
|
+
|
|
152
|
+
return { removedProvider: hadProvider, removedModel: hadModel, config: ocConfig }
|
|
153
|
+
}
|
|
154
|
+
|
|
131
155
|
/**
|
|
132
156
|
* MERGE the single FCM proxy provider into OpenCode config.
|
|
133
157
|
*
|
|
@@ -157,3 +181,20 @@ export function syncToOpenCode(fcmConfig, _sources, mergedModels, proxyInfo = {}
|
|
|
157
181
|
path: OC_CONFIG_PATH,
|
|
158
182
|
}
|
|
159
183
|
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Remove the persisted FCM proxy provider from OpenCode's config on disk.
|
|
187
|
+
* This is the user-facing cleanup operation for "proxy uninstall".
|
|
188
|
+
*
|
|
189
|
+
* @returns {{ removedProvider: boolean, removedModel: boolean, path: string }}
|
|
190
|
+
*/
|
|
191
|
+
export function cleanupOpenCodeProxyConfig() {
|
|
192
|
+
const oc = loadOpenCodeConfig()
|
|
193
|
+
const result = removeFcmProxyFromConfig(oc)
|
|
194
|
+
saveOpenCodeConfig(result.config)
|
|
195
|
+
return {
|
|
196
|
+
removedProvider: result.removedProvider,
|
|
197
|
+
removedModel: result.removedModel,
|
|
198
|
+
path: OC_CONFIG_PATH,
|
|
199
|
+
}
|
|
200
|
+
}
|
package/src/opencode.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* - `startProxyAndLaunch` — Start fcm-proxy then launch OpenCode
|
|
24
24
|
* - `autoStartProxyIfSynced` — Auto-start proxy if opencode.json has fcm-proxy
|
|
25
25
|
* - `ensureProxyRunning` — Ensure proxy is running (start or reuse)
|
|
26
|
+
* - `isProxyEnabledForConfig` — Check whether proxy mode is opted in
|
|
26
27
|
*
|
|
27
28
|
* @see src/opencode-sync.js — syncToOpenCode/load/save utilities
|
|
28
29
|
* @see src/proxy-server.js — ProxyServer implementation
|
|
@@ -40,7 +41,7 @@ import { sources } from '../sources.js'
|
|
|
40
41
|
import { resolveCloudflareUrl } from './ping.js'
|
|
41
42
|
import { ProxyServer } from './proxy-server.js'
|
|
42
43
|
import { loadOpenCodeConfig, saveOpenCodeConfig, syncToOpenCode } from './opencode-sync.js'
|
|
43
|
-
import { getApiKey, resolveApiKeys } from './config.js'
|
|
44
|
+
import { getApiKey, getProxySettings, resolveApiKeys } from './config.js'
|
|
44
45
|
import { ENV_VAR_NAMES, OPENCODE_MODEL_MAP, isWindows, isMac, isLinux } from './provider-metadata.js'
|
|
45
46
|
import { setActiveProxy } from './render-table.js'
|
|
46
47
|
|
|
@@ -559,10 +560,25 @@ export function buildProxyTopologyFromConfig(fcmConfig) {
|
|
|
559
560
|
return { accounts, proxyModels }
|
|
560
561
|
}
|
|
561
562
|
|
|
563
|
+
/**
|
|
564
|
+
* 📖 Proxy mode is opt-in. Both launch-time proxying and persistent sync rely on
|
|
565
|
+
* 📖 this single helper so settings/profile changes behave consistently.
|
|
566
|
+
*
|
|
567
|
+
* @param {object} fcmConfig
|
|
568
|
+
* @returns {boolean}
|
|
569
|
+
*/
|
|
570
|
+
export function isProxyEnabledForConfig(fcmConfig) {
|
|
571
|
+
return getProxySettings(fcmConfig).enabled === true
|
|
572
|
+
}
|
|
573
|
+
|
|
562
574
|
export async function ensureProxyRunning(fcmConfig, { forceRestart = false } = {}) {
|
|
563
575
|
registerExitHandlers()
|
|
564
576
|
proxyCleanedUp = false
|
|
565
577
|
|
|
578
|
+
if (!isProxyEnabledForConfig(fcmConfig)) {
|
|
579
|
+
throw new Error('Proxy mode is disabled in Settings')
|
|
580
|
+
}
|
|
581
|
+
|
|
566
582
|
if (forceRestart && activeProxy) {
|
|
567
583
|
await cleanupProxy()
|
|
568
584
|
}
|
|
@@ -587,7 +603,9 @@ export async function ensureProxyRunning(fcmConfig, { forceRestart = false } = {
|
|
|
587
603
|
}
|
|
588
604
|
|
|
589
605
|
const proxyToken = `fcm_${randomUUID().replace(/-/g, '')}`
|
|
590
|
-
const
|
|
606
|
+
const proxySettings = getProxySettings(fcmConfig)
|
|
607
|
+
const preferredPort = Number.isInteger(proxySettings.preferredPort) ? proxySettings.preferredPort : 0
|
|
608
|
+
const proxy = new ProxyServer({ port: preferredPort, accounts, proxyApiKey: proxyToken })
|
|
591
609
|
const { port } = await proxy.start()
|
|
592
610
|
activeProxy = proxy
|
|
593
611
|
setActiveProxy(activeProxy)
|
|
@@ -598,6 +616,9 @@ export async function ensureProxyRunning(fcmConfig, { forceRestart = false } = {
|
|
|
598
616
|
|
|
599
617
|
export async function autoStartProxyIfSynced(fcmConfig, state) {
|
|
600
618
|
try {
|
|
619
|
+
const proxySettings = getProxySettings(fcmConfig)
|
|
620
|
+
if (!proxySettings.enabled || !proxySettings.syncToOpenCode) return
|
|
621
|
+
|
|
601
622
|
const ocConfig = loadOpenCodeConfig()
|
|
602
623
|
if (!ocConfig?.provider?.['fcm-proxy']) return
|
|
603
624
|
|
package/src/overlays.js
CHANGED
|
@@ -23,6 +23,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
23
23
|
PROVIDER_METADATA,
|
|
24
24
|
LOCAL_VERSION,
|
|
25
25
|
getApiKey,
|
|
26
|
+
getProxySettings,
|
|
26
27
|
resolveApiKeys,
|
|
27
28
|
isProviderEnabled,
|
|
28
29
|
listProfiles,
|
|
@@ -49,6 +50,16 @@ export function createOverlayRenderers(state, deps) {
|
|
|
49
50
|
getPingModel,
|
|
50
51
|
} = deps
|
|
51
52
|
|
|
53
|
+
// 📖 Keep log token formatting aligned with the main table so the same totals
|
|
54
|
+
// 📖 read the same everywhere in the TUI.
|
|
55
|
+
const formatLogTokens = (totalTokens) => {
|
|
56
|
+
const safeTotal = Number(totalTokens) || 0
|
|
57
|
+
if (safeTotal <= 0) return '--'
|
|
58
|
+
if (safeTotal >= 999_500) return `${(safeTotal / 1_000_000).toFixed(2)}M`
|
|
59
|
+
if (safeTotal >= 1_000) return `${(safeTotal / 1_000).toFixed(2)}k`
|
|
60
|
+
return String(Math.floor(safeTotal))
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
// ─── Settings screen renderer ─────────────────────────────────────────────
|
|
53
64
|
// 📖 renderSettings: Draw the settings overlay in the alt screen buffer.
|
|
54
65
|
// 📖 Shows all providers with their API key (masked) + enabled state.
|
|
@@ -57,6 +68,11 @@ export function createOverlayRenderers(state, deps) {
|
|
|
57
68
|
function renderSettings() {
|
|
58
69
|
const providerKeys = Object.keys(sources)
|
|
59
70
|
const updateRowIdx = providerKeys.length
|
|
71
|
+
const proxyEnabledRowIdx = updateRowIdx + 1
|
|
72
|
+
const proxySyncRowIdx = updateRowIdx + 2
|
|
73
|
+
const proxyPortRowIdx = updateRowIdx + 3
|
|
74
|
+
const proxyCleanupRowIdx = updateRowIdx + 4
|
|
75
|
+
const proxySettings = getProxySettings(state.config)
|
|
60
76
|
const EL = '\x1b[K'
|
|
61
77
|
const lines = []
|
|
62
78
|
const cursorLineByRow = {}
|
|
@@ -163,9 +179,37 @@ export function createOverlayRenderers(state, deps) {
|
|
|
163
179
|
lines.push(chalk.red(` ${state.settingsUpdateError}`))
|
|
164
180
|
}
|
|
165
181
|
|
|
182
|
+
lines.push('')
|
|
183
|
+
lines.push(` ${chalk.bold('🔀 Proxy')}`)
|
|
184
|
+
lines.push(` ${chalk.dim(' ' + '─'.repeat(112))}`)
|
|
185
|
+
lines.push('')
|
|
186
|
+
|
|
187
|
+
const proxyEnabledBullet = state.settingsCursor === proxyEnabledRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
188
|
+
const proxyEnabledRow = `${proxyEnabledBullet}${chalk.bold('Proxy mode (opt-in)').padEnd(44)} ${proxySettings.enabled ? chalk.greenBright('Enabled') : chalk.dim('Disabled by default')}`
|
|
189
|
+
cursorLineByRow[proxyEnabledRowIdx] = lines.length
|
|
190
|
+
lines.push(state.settingsCursor === proxyEnabledRowIdx ? chalk.bgRgb(20, 45, 60)(proxyEnabledRow) : proxyEnabledRow)
|
|
191
|
+
|
|
192
|
+
const proxySyncBullet = state.settingsCursor === proxySyncRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
193
|
+
const proxySyncRow = `${proxySyncBullet}${chalk.bold('Persist proxy in OpenCode').padEnd(44)} ${proxySettings.syncToOpenCode ? chalk.greenBright('Enabled') : chalk.dim('Disabled')}`
|
|
194
|
+
cursorLineByRow[proxySyncRowIdx] = lines.length
|
|
195
|
+
lines.push(state.settingsCursor === proxySyncRowIdx ? chalk.bgRgb(20, 45, 60)(proxySyncRow) : proxySyncRow)
|
|
196
|
+
|
|
197
|
+
const proxyPortBullet = state.settingsCursor === proxyPortRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
198
|
+
const proxyPortValue = state.settingsProxyPortEditMode && state.settingsCursor === proxyPortRowIdx
|
|
199
|
+
? chalk.cyanBright(`${state.settingsProxyPortBuffer}▏`)
|
|
200
|
+
: (proxySettings.preferredPort === 0 ? chalk.dim('auto (OS-assigned)') : chalk.green(String(proxySettings.preferredPort)))
|
|
201
|
+
const proxyPortRow = `${proxyPortBullet}${chalk.bold('Preferred proxy port').padEnd(44)} ${proxyPortValue}`
|
|
202
|
+
cursorLineByRow[proxyPortRowIdx] = lines.length
|
|
203
|
+
lines.push(state.settingsCursor === proxyPortRowIdx ? chalk.bgRgb(20, 45, 60)(proxyPortRow) : proxyPortRow)
|
|
204
|
+
|
|
205
|
+
const proxyCleanupBullet = state.settingsCursor === proxyCleanupRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
206
|
+
const proxyCleanupRow = `${proxyCleanupBullet}${chalk.bold('Clean OpenCode proxy config').padEnd(44)} ${chalk.dim('Enter removes fcm-proxy from opencode.json')}`
|
|
207
|
+
cursorLineByRow[proxyCleanupRowIdx] = lines.length
|
|
208
|
+
lines.push(state.settingsCursor === proxyCleanupRowIdx ? chalk.bgRgb(45, 30, 30)(proxyCleanupRow) : proxyCleanupRow)
|
|
209
|
+
|
|
166
210
|
// 📖 Profiles section — list saved profiles with active indicator + delete support
|
|
167
211
|
const savedProfiles = listProfiles(state.config)
|
|
168
|
-
const profileStartIdx = updateRowIdx +
|
|
212
|
+
const profileStartIdx = updateRowIdx + 5
|
|
169
213
|
const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : updateRowIdx
|
|
170
214
|
|
|
171
215
|
lines.push('')
|
|
@@ -194,8 +238,10 @@ export function createOverlayRenderers(state, deps) {
|
|
|
194
238
|
lines.push('')
|
|
195
239
|
if (state.settingsEditMode) {
|
|
196
240
|
lines.push(chalk.dim(' Type API key • Enter Save • Esc Cancel'))
|
|
241
|
+
} else if (state.settingsProxyPortEditMode) {
|
|
242
|
+
lines.push(chalk.dim(' Type proxy port (0 = auto) • Enter Save • Esc Cancel'))
|
|
197
243
|
} else {
|
|
198
|
-
lines.push(chalk.dim(' ↑↓ Navigate • Enter Edit
|
|
244
|
+
lines.push(chalk.dim(' ↑↓ Navigate • Enter Edit/Run • + Add key • - Remove key • Space Toggle • T Test key • S Sync→OpenCode • R Restore backup • U Updates • ⌫ Delete profile • Esc Close'))
|
|
199
245
|
}
|
|
200
246
|
// 📖 Show sync/restore status message if set
|
|
201
247
|
if (state.settingsSyncStatus) {
|
|
@@ -283,12 +329,12 @@ export function createOverlayRenderers(state, deps) {
|
|
|
283
329
|
lines.push(` ${chalk.yellow('W')} Toggle ping mode ${chalk.dim('(speed 2s → normal 10s → slow 30s → forced 4s)')}`)
|
|
284
330
|
lines.push(` ${chalk.yellow('E')} Toggle configured models only ${chalk.dim('(enabled by default, persisted globally + in profiles)')}`)
|
|
285
331
|
lines.push(` ${chalk.yellow('X')} Toggle token log page ${chalk.dim('(shows recent request usage from request-log.jsonl)')}`)
|
|
286
|
-
lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode
|
|
332
|
+
lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode → Desktop → OpenClaw → Crush → Goose)')}`)
|
|
287
333
|
lines.push(` ${chalk.yellow('F')} Toggle favorite on selected row ${chalk.dim('(⭐ pinned at top, persisted)')}`)
|
|
288
334
|
lines.push(` ${chalk.yellow('Q')} Smart Recommend ${chalk.dim('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
|
|
289
335
|
lines.push(` ${chalk.rgb(57, 255, 20).bold('J')} Request Feature ${chalk.dim('(📝 send anonymous feedback to the project team)')}`)
|
|
290
336
|
lines.push(` ${chalk.rgb(255, 87, 51).bold('I')} Report Bug ${chalk.dim('(🐛 send anonymous bug report to the project team)')}`)
|
|
291
|
-
lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, manual update)')}`)
|
|
337
|
+
lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, proxy, manual update)')}`)
|
|
292
338
|
lines.push(` ${chalk.yellow('Shift+P')} Cycle config profile ${chalk.dim('(switch between saved profiles live)')}`)
|
|
293
339
|
lines.push(` ${chalk.yellow('Shift+S')} Save current config as a named profile ${chalk.dim('(inline prompt — type name + Enter)')}`)
|
|
294
340
|
lines.push(` ${chalk.dim('Profiles store: favorites, sort, tier filter, ping interval, configured-only filter, API keys.')}`)
|
|
@@ -311,12 +357,24 @@ export function createOverlayRenderers(state, deps) {
|
|
|
311
357
|
lines.push(` ${chalk.cyan('free-coding-models --opencode')} ${chalk.dim('OpenCode CLI mode')}`)
|
|
312
358
|
lines.push(` ${chalk.cyan('free-coding-models --opencode-desktop')} ${chalk.dim('OpenCode Desktop mode')}`)
|
|
313
359
|
lines.push(` ${chalk.cyan('free-coding-models --openclaw')} ${chalk.dim('OpenClaw mode')}`)
|
|
360
|
+
lines.push(` ${chalk.cyan('free-coding-models --crush')} ${chalk.dim('Crush mode')}`)
|
|
361
|
+
lines.push(` ${chalk.cyan('free-coding-models --goose')} ${chalk.dim('Goose mode')}`)
|
|
362
|
+
// 📖 Temporarily disabled launchers kept out of the public help until their flows are hardened.
|
|
363
|
+
// lines.push(` ${chalk.cyan('free-coding-models --aider')} ${chalk.dim('Aider mode')}`)
|
|
364
|
+
// lines.push(` ${chalk.cyan('free-coding-models --claude-code')} ${chalk.dim('Claude Code proxy mode')}`)
|
|
365
|
+
// lines.push(` ${chalk.cyan('free-coding-models --codex')} ${chalk.dim('Codex CLI proxy mode')}`)
|
|
366
|
+
// lines.push(` ${chalk.cyan('free-coding-models --gemini')} ${chalk.dim('Gemini CLI proxy mode')}`)
|
|
367
|
+
// lines.push(` ${chalk.cyan('free-coding-models --qwen')} ${chalk.dim('Qwen Code mode')}`)
|
|
368
|
+
// lines.push(` ${chalk.cyan('free-coding-models --openhands')} ${chalk.dim('OpenHands mode')}`)
|
|
369
|
+
// lines.push(` ${chalk.cyan('free-coding-models --amp')} ${chalk.dim('Amp mode')}`)
|
|
370
|
+
// lines.push(` ${chalk.cyan('free-coding-models --pi')} ${chalk.dim('Pi mode')}`)
|
|
314
371
|
lines.push(` ${chalk.cyan('free-coding-models --best')} ${chalk.dim('Only top tiers (A+, S, S+)')}`)
|
|
315
372
|
lines.push(` ${chalk.cyan('free-coding-models --fiable')} ${chalk.dim('10s reliability analysis')}`)
|
|
316
373
|
lines.push(` ${chalk.cyan('free-coding-models --tier S|A|B|C')} ${chalk.dim('Filter by tier letter')}`)
|
|
317
374
|
lines.push(` ${chalk.cyan('free-coding-models --no-telemetry')} ${chalk.dim('Disable telemetry for this run')}`)
|
|
318
375
|
lines.push(` ${chalk.cyan('free-coding-models --recommend')} ${chalk.dim('Auto-open Smart Recommend on start')}`)
|
|
319
376
|
lines.push(` ${chalk.cyan('free-coding-models --profile <name>')} ${chalk.dim('Load a saved config profile')}`)
|
|
377
|
+
lines.push(` ${chalk.cyan('free-coding-models --clean-proxy')} ${chalk.dim('Remove persisted fcm-proxy config from OpenCode')}`)
|
|
320
378
|
lines.push(` ${chalk.dim('Flags can be combined: --openclaw --tier S')}`)
|
|
321
379
|
lines.push('')
|
|
322
380
|
// 📖 Help overlay can be longer than viewport, so keep a dedicated scroll offset.
|
|
@@ -336,35 +394,39 @@ export function createOverlayRenderers(state, deps) {
|
|
|
336
394
|
const lines = []
|
|
337
395
|
lines.push('')
|
|
338
396
|
lines.push(` ${chalk.bold('📋 Request Log')} ${chalk.dim('— recent requests • ↑↓ scroll • X or Esc close')}`)
|
|
397
|
+
lines.push(chalk.dim(' Works only when the multi-account proxy is enabled and requests go through it.'))
|
|
398
|
+
lines.push(chalk.dim(' Direct provider launches do not currently write into this log.'))
|
|
339
399
|
lines.push('')
|
|
340
400
|
|
|
341
401
|
// 📖 Load recent log entries — bounded read, newest-first, malformed lines skipped.
|
|
342
402
|
const logRows = loadRecentLogs({ limit: 200 })
|
|
403
|
+
const totalTokens = logRows.reduce((sum, row) => sum + (Number(row.tokens) || 0), 0)
|
|
343
404
|
|
|
344
405
|
if (logRows.length === 0) {
|
|
345
406
|
lines.push(chalk.dim(' No log entries found.'))
|
|
346
407
|
lines.push(chalk.dim(' Logs are written to ~/.free-coding-models/request-log.jsonl'))
|
|
347
408
|
lines.push(chalk.dim(' when requests are proxied through the multi-account rotation proxy.'))
|
|
409
|
+
lines.push(chalk.dim(' Direct provider launches do not currently feed this token log.'))
|
|
348
410
|
} else {
|
|
411
|
+
lines.push(` ${chalk.bold('Total Consumed:')} ${chalk.greenBright(formatLogTokens(totalTokens))}`)
|
|
412
|
+
lines.push('')
|
|
349
413
|
// 📖 Column widths for the log table
|
|
350
414
|
const W_TIME = 19
|
|
351
|
-
const W_TYPE = 18
|
|
352
415
|
const W_PROV = 14
|
|
353
416
|
const W_MODEL = 36
|
|
354
417
|
const W_STATUS = 8
|
|
355
|
-
const W_TOKENS =
|
|
418
|
+
const W_TOKENS = 12
|
|
356
419
|
const W_LAT = 10
|
|
357
420
|
|
|
358
421
|
// 📖 Header row
|
|
359
422
|
const hTime = chalk.dim('Time'.padEnd(W_TIME))
|
|
360
|
-
const hType = chalk.dim('Type'.padEnd(W_TYPE))
|
|
361
423
|
const hProv = chalk.dim('Provider'.padEnd(W_PROV))
|
|
362
424
|
const hModel = chalk.dim('Model'.padEnd(W_MODEL))
|
|
363
425
|
const hStatus = chalk.dim('Status'.padEnd(W_STATUS))
|
|
364
|
-
const hTok = chalk.dim('Used'.padEnd(W_TOKENS))
|
|
426
|
+
const hTok = chalk.dim('Tokens Used'.padEnd(W_TOKENS))
|
|
365
427
|
const hLat = chalk.dim('Latency'.padEnd(W_LAT))
|
|
366
|
-
lines.push(` ${hTime} ${
|
|
367
|
-
lines.push(chalk.dim(' ' + '─'.repeat(W_TIME +
|
|
428
|
+
lines.push(` ${hTime} ${hProv} ${hModel} ${hStatus} ${hTok} ${hLat}`)
|
|
429
|
+
lines.push(chalk.dim(' ' + '─'.repeat(W_TIME + W_PROV + W_MODEL + W_STATUS + W_TOKENS + W_LAT + 10)))
|
|
368
430
|
|
|
369
431
|
for (const row of logRows) {
|
|
370
432
|
// 📖 Format time as HH:MM:SS (strip the date part for compactness)
|
|
@@ -389,17 +451,16 @@ export function createOverlayRenderers(state, deps) {
|
|
|
389
451
|
statusCell = chalk.dim(sc.padEnd(W_STATUS))
|
|
390
452
|
}
|
|
391
453
|
|
|
392
|
-
const tokStr =
|
|
454
|
+
const tokStr = formatLogTokens(row.tokens)
|
|
393
455
|
const latStr = row.latency > 0 ? `${row.latency}ms` : '--'
|
|
394
456
|
|
|
395
457
|
const timeCell = chalk.dim(timeStr.slice(0, W_TIME).padEnd(W_TIME))
|
|
396
|
-
const typeCell = chalk.magenta((row.requestType || '--').slice(0, W_TYPE).padEnd(W_TYPE))
|
|
397
458
|
const provCell = chalk.cyan(row.provider.slice(0, W_PROV).padEnd(W_PROV))
|
|
398
459
|
const modelCell = chalk.white(row.model.slice(0, W_MODEL).padEnd(W_MODEL))
|
|
399
460
|
const tokCell = chalk.dim(tokStr.padEnd(W_TOKENS))
|
|
400
461
|
const latCell = chalk.dim(latStr.padEnd(W_LAT))
|
|
401
462
|
|
|
402
|
-
lines.push(` ${timeCell} ${
|
|
463
|
+
lines.push(` ${timeCell} ${provCell} ${modelCell} ${statusCell} ${tokCell} ${latCell}`)
|
|
403
464
|
}
|
|
404
465
|
}
|
|
405
466
|
|