free-coding-models 0.3.9 → 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.
@@ -13,10 +13,6 @@
13
13
  * - it merges into existing config files instead of replacing them
14
14
  * - it records successful installs in `~/.free-coding-models.json` so catalogs can be refreshed automatically later
15
15
  *
16
- * 📖 Connection modes:
17
- * - `direct` — connect the tool straight to the provider API endpoint (no proxy)
18
- * - `proxy` — route through the local FCM proxy (key rotation + usage tracking)
19
- *
20
16
  * 📖 Tool-specific notes:
21
17
  * - OpenCode CLI and OpenCode Desktop share the same `opencode.json`
22
18
  * - Crush gets a managed provider block in `crush.json`
@@ -25,7 +21,6 @@
25
21
  * - Pi gets models.json + settings.json under ~/.pi/agent/
26
22
  * - Aider gets ~/.aider.conf.yml with OpenAI-compatible config
27
23
  * - Amp gets ~/.config/amp/settings.json
28
- * - Gemini gets ~/.gemini/settings.json
29
24
  * - Qwen gets ~/.qwen/settings.json with modelProviders
30
25
  * - OpenHands gets a sourceable env file (~/.fcm-openhands-env)
31
26
  *
@@ -49,21 +44,15 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from
49
44
  import { homedir } from 'node:os'
50
45
  import { dirname, join } from 'node:path'
51
46
  import { MODELS, sources } from '../sources.js'
52
- import { getApiKey, saveConfig, getProxySettings, loadConfig } from './config.js'
47
+ import { getApiKey, saveConfig } from './config.js'
53
48
  import { ENV_VAR_NAMES, PROVIDER_METADATA } from './provider-metadata.js'
54
49
  import { getToolMeta } from './tool-metadata.js'
55
50
 
56
51
  const DIRECT_INSTALL_UNSUPPORTED_PROVIDERS = new Set(['replicate', 'zai'])
57
52
  // 📖 Install Endpoints only lists tools whose persisted config shape is actually supported here.
58
- // 📖 Claude Code, Codex, and Gemini stay launcher-only until their proxy/runtime setup is stable.
53
+ // 📖 Claude Code, Codex, and Gemini stay out while their dedicated bridges are being rebuilt.
59
54
  const INSTALL_TARGET_MODES = ['opencode', 'opencode-desktop', 'openclaw', 'crush', 'goose', 'pi', 'aider', 'qwen', 'openhands', 'amp']
60
55
 
61
- // 📖 Connection modes: direct (pure provider) vs FCM proxy (rotates keys)
62
- export const CONNECTION_MODES = [
63
- { key: 'direct', label: 'Direct Provider', hint: 'Connect the tool straight to the provider API — no proxy involved.' },
64
- { key: 'proxy', label: 'FCM Proxy V2', hint: 'Route through FCM Proxy V2 with key rotation and usage tracking.' },
65
- ]
66
-
67
56
  function getDefaultPaths() {
68
57
  const home = homedir()
69
58
  return {
@@ -76,7 +65,6 @@ function getDefaultPaths() {
76
65
  piSettingsPath: join(home, '.pi', 'agent', 'settings.json'),
77
66
  aiderConfigPath: join(home, '.aider.conf.yml'),
78
67
  ampConfigPath: join(home, '.config', 'amp', 'settings.json'),
79
- geminiConfigPath: join(home, '.gemini', 'settings.json'),
80
68
  qwenConfigPath: join(home, '.qwen', 'settings.json'),
81
69
  }
82
70
  }
@@ -199,7 +187,7 @@ function getDirectInstallSupport(providerKey) {
199
187
  return { supported: false, reason: 'Unknown provider' }
200
188
  }
201
189
  if (DIRECT_INSTALL_UNSUPPORTED_PROVIDERS.has(providerKey)) {
202
- return { supported: false, reason: 'This provider needs a non-standard proxy/runtime bridge' }
190
+ return { supported: false, reason: 'This provider still needs a dedicated runtime bridge' }
203
191
  }
204
192
  if (providerKey === 'cloudflare' && !(process.env.CLOUDFLARE_ACCOUNT_ID || '').trim()) {
205
193
  return { supported: false, reason: 'CLOUDFLARE_ACCOUNT_ID is required for direct installs' }
@@ -360,13 +348,17 @@ function installIntoOpenClaw(providerKey, models, apiKey, paths) {
360
348
  const filePath = paths.openclawConfigPath
361
349
  const providerId = getManagedProviderId(providerKey)
362
350
  const config = readJson(filePath, {})
351
+ const primaryModel = models[0]
352
+ const primaryModelRef = primaryModel ? `${providerId}/${primaryModel.modelId}` : null
363
353
 
364
354
  if (!config.models || typeof config.models !== 'object') config.models = {}
365
355
  if (config.models.mode !== 'replace') config.models.mode = 'merge'
366
356
  if (!config.models.providers || typeof config.models.providers !== 'object') config.models.providers = {}
367
357
  if (!config.agents || typeof config.agents !== 'object') config.agents = {}
368
358
  if (!config.agents.defaults || typeof config.agents.defaults !== 'object') config.agents.defaults = {}
359
+ if (!config.agents.defaults.model || typeof config.agents.defaults.model !== 'object') config.agents.defaults.model = {}
369
360
  if (!config.agents.defaults.models || typeof config.agents.defaults.models !== 'object') config.agents.defaults.models = {}
361
+ if (!config.env || typeof config.env !== 'object') config.env = {}
370
362
 
371
363
  config.models.providers[providerId] = {
372
364
  baseUrl: resolveProviderBaseUrl(providerKey),
@@ -394,8 +386,17 @@ function installIntoOpenClaw(providerKey, models, apiKey, paths) {
394
386
  config.agents.defaults.models[`${providerId}/${model.modelId}`] = {}
395
387
  }
396
388
 
389
+ if (primaryModelRef) {
390
+ config.agents.defaults.model.primary = primaryModelRef
391
+ }
392
+
393
+ const providerEnvName = ENV_VAR_NAMES[providerKey]
394
+ if (providerEnvName && apiKey) {
395
+ config.env[providerEnvName] = apiKey
396
+ }
397
+
397
398
  const backupPath = writeJson(filePath, config)
398
- return { path: filePath, backupPath, providerId, modelCount: models.length }
399
+ return { path: filePath, backupPath, providerId, modelCount: models.length, primaryModelRef }
399
400
  }
400
401
 
401
402
  // 📖 installIntoPi writes models.json + settings.json under ~/.pi/agent/
@@ -453,15 +454,6 @@ function installIntoAmp(providerKey, models, apiKey, paths) {
453
454
  return { path: paths.ampConfigPath, backupPath, providerId, modelCount: models.length }
454
455
  }
455
456
 
456
- // 📖 installIntoGemini writes ~/.gemini/settings.json with model ID
457
- function installIntoGemini(providerKey, models, apiKey, paths) {
458
- const providerId = getManagedProviderId(providerKey)
459
- const config = readJson(paths.geminiConfigPath, {})
460
- config.model = models[0]?.modelId ?? ''
461
- const backupPath = writeJson(paths.geminiConfigPath, config)
462
- return { path: paths.geminiConfigPath, backupPath, providerId, modelCount: models.length }
463
- }
464
-
465
457
  // 📖 installIntoQwen writes ~/.qwen/settings.json with modelProviders config
466
458
  function installIntoQwen(providerKey, models, apiKey, paths) {
467
459
  const providerId = getManagedProviderId(providerKey)
@@ -486,36 +478,22 @@ function installIntoQwen(providerKey, models, apiKey, paths) {
486
478
  return { path: paths.qwenConfigPath, backupPath, providerId, modelCount: models.length }
487
479
  }
488
480
 
489
- // 📖 installIntoEnvBasedTool handles tools that rely on env vars only (claude-code, codex, openhands).
481
+ // 📖 installIntoEnvBasedTool handles tools that rely on env vars only.
490
482
  // 📖 We write a small .env-style helper file so users can source it before launching.
491
- // 📖 When connectionMode is 'proxy', writes env vars pointing to the daemon's stable port/token.
492
- function installIntoEnvBasedTool(providerKey, models, apiKey, toolMode, paths, connectionMode = 'direct') {
483
+ function installIntoEnvBasedTool(providerKey, models, apiKey, toolMode) {
493
484
  const providerId = getManagedProviderId(providerKey)
494
485
  const home = homedir()
495
486
  const envFileName = `.fcm-${toolMode}-env`
496
487
  const envFilePath = join(home, envFileName)
497
488
  const primaryModel = models[0]
498
-
499
- // 📖 Resolve effective API key, base URL, and model ID based on connection mode
500
- let effectiveApiKey = apiKey
501
- let effectiveBaseUrl = resolveProviderBaseUrl(providerKey)
502
- let effectiveModelId = primaryModel.modelId
503
-
504
- if (connectionMode === 'proxy') {
505
- // 📖 Read stable proxy settings from config for daemon-compatible env files
506
- try {
507
- const cfg = loadConfig()
508
- const proxySettings = getProxySettings(cfg)
509
- effectiveApiKey = proxySettings.stableToken || apiKey
510
- const port = proxySettings.preferredPort || 18045
511
- effectiveBaseUrl = `http://127.0.0.1:${port}/v1`
512
- } catch { /* fallback to direct values */ }
513
- }
489
+ const effectiveApiKey = apiKey
490
+ const effectiveBaseUrl = resolveProviderBaseUrl(providerKey)
491
+ const effectiveModelId = primaryModel.modelId
514
492
 
515
493
  const envLines = [
516
494
  '# 📖 Managed by free-coding-models — source this file before launching the tool',
517
495
  `# 📖 Provider: ${getProviderLabel(providerKey)} (${models.length} models)`,
518
- `# 📖 Connection: ${connectionMode === 'proxy' ? 'FCM Proxy V2 (background service)' : 'Direct provider'}`,
496
+ '# 📖 Connection: Direct provider',
519
497
  `export OPENAI_API_KEY="${effectiveApiKey}"`,
520
498
  `export OPENAI_BASE_URL="${effectiveBaseUrl}"`,
521
499
  `export OPENAI_MODEL="${effectiveModelId}"`,
@@ -524,19 +502,6 @@ function installIntoEnvBasedTool(providerKey, models, apiKey, toolMode, paths, c
524
502
  `export LLM_MODEL="openai/${effectiveModelId}"`,
525
503
  ]
526
504
 
527
- // 📖 Claude Code: Anthropic-specific env vars pointing to proxy /v1/messages endpoint
528
- if (toolMode === 'claude-code') {
529
- if (connectionMode === 'proxy') {
530
- // 📖 Point to proxy base (not /v1) — Claude Code adds /v1/messages itself
531
- const proxyBase = effectiveBaseUrl.replace(/\/v1$/, '')
532
- envLines.push(`export ANTHROPIC_AUTH_TOKEN="${effectiveApiKey}"`)
533
- envLines.push(`export ANTHROPIC_BASE_URL="${proxyBase}"`)
534
- } else {
535
- envLines.push(`export ANTHROPIC_AUTH_TOKEN="${effectiveApiKey}"`)
536
- envLines.push(`export ANTHROPIC_BASE_URL="${effectiveBaseUrl}"`)
537
- }
538
- }
539
-
540
505
  ensureDirFor(envFilePath)
541
506
  const backupPath = backupIfExists(envFilePath)
542
507
  writeFileSync(envFilePath, envLines.join('\n') + '\n')
@@ -545,7 +510,6 @@ function installIntoEnvBasedTool(providerKey, models, apiKey, toolMode, paths, c
545
510
 
546
511
  export function installProviderEndpoints(config, providerKey, toolMode, options = {}) {
547
512
  const canonicalToolMode = canonicalizeToolMode(toolMode)
548
- const connectionMode = options.connectionMode || 'direct'
549
513
  const support = getDirectInstallSupport(providerKey)
550
514
  if (!support.supported) {
551
515
  throw new Error(support.reason || 'Direct install is not supported for this provider')
@@ -575,12 +539,10 @@ export function installProviderEndpoints(config, providerKey, toolMode, options
575
539
  installResult = installIntoAider(providerKey, models, apiKey, paths)
576
540
  } else if (canonicalToolMode === 'amp') {
577
541
  installResult = installIntoAmp(providerKey, models, apiKey, paths)
578
- } else if (canonicalToolMode === 'gemini') {
579
- installResult = installIntoGemini(providerKey, models, apiKey, paths)
580
542
  } else if (canonicalToolMode === 'qwen') {
581
543
  installResult = installIntoQwen(providerKey, models, apiKey, paths)
582
- } else if (canonicalToolMode === 'claude-code' || canonicalToolMode === 'codex' || canonicalToolMode === 'openhands') {
583
- installResult = installIntoEnvBasedTool(providerKey, models, apiKey, canonicalToolMode, paths, connectionMode)
544
+ } else if (canonicalToolMode === 'openhands') {
545
+ installResult = installIntoEnvBasedTool(providerKey, models, apiKey, canonicalToolMode, paths)
584
546
  } else {
585
547
  throw new Error(`Unsupported install target: ${toolMode}`)
586
548
  }
@@ -597,7 +559,7 @@ export function installProviderEndpoints(config, providerKey, toolMode, options
597
559
  providerKey,
598
560
  providerLabel: getProviderLabel(providerKey),
599
561
  scope,
600
- connectionMode,
562
+ connectionMode: 'direct',
601
563
  autoRefreshEnabled: true,
602
564
  models,
603
565
  }
package/src/favorites.js CHANGED
@@ -85,34 +85,20 @@ export function syncFavoriteFlags(results, config) {
85
85
  */
86
86
  export function toggleFavoriteModel(config, providerKey, modelId) {
87
87
  const latestConfig = loadConfig()
88
- latestConfig.activeProfile = typeof config?.activeProfile === 'string' && config.activeProfile.trim()
89
- ? config.activeProfile.trim()
90
- : latestConfig.activeProfile
91
88
  ensureFavoritesConfig(latestConfig)
92
- if (latestConfig.activeProfile && !latestConfig.profiles?.[latestConfig.activeProfile] && config?.profiles?.[latestConfig.activeProfile]) {
93
- latestConfig.profiles[latestConfig.activeProfile] = JSON.parse(JSON.stringify(config.profiles[latestConfig.activeProfile]))
94
- }
95
89
  const favoriteKey = toFavoriteKey(providerKey, modelId)
96
90
  const existingIndex = latestConfig.favorites.indexOf(favoriteKey)
97
91
  if (existingIndex >= 0) {
98
92
  latestConfig.favorites.splice(existingIndex, 1)
99
- if (latestConfig.activeProfile && latestConfig.profiles?.[latestConfig.activeProfile]) {
100
- latestConfig.profiles[latestConfig.activeProfile].favorites = [...latestConfig.favorites]
101
- }
102
93
  const saveResult = saveConfig(latestConfig, {
103
94
  replaceFavorites: true,
104
- replaceProfileNames: latestConfig.activeProfile ? [latestConfig.activeProfile] : [],
105
95
  })
106
96
  if (saveResult.success) replaceConfigContents(config, latestConfig)
107
97
  return false
108
98
  }
109
99
  latestConfig.favorites.push(favoriteKey)
110
- if (latestConfig.activeProfile && latestConfig.profiles?.[latestConfig.activeProfile]) {
111
- latestConfig.profiles[latestConfig.activeProfile].favorites = [...latestConfig.favorites]
112
- }
113
100
  const saveResult = saveConfig(latestConfig, {
114
101
  replaceFavorites: true,
115
- replaceProfileNames: latestConfig.activeProfile ? [latestConfig.activeProfile] : [],
116
102
  })
117
103
  if (saveResult.success) replaceConfigContents(config, latestConfig)
118
104
  return true