free-coding-models 0.5.37 โ†’ 0.5.39

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.
@@ -90,6 +90,8 @@ function getDefaultToolPaths(homeDir = homedir()) {
90
90
  continueConfigPath: join(homeDir, '.continue', 'config.yaml'),
91
91
  clineConfigPath: join(homeDir, '.cline', 'globalState.json'),
92
92
  forgeCodeConfigPath: join(homeDir, '.forge', '.forge.toml'),
93
+ zcodeConfigPath: join(homeDir, '.zcode', 'v2', 'config.json'),
94
+ zcodeModelCachePath: join(homeDir, '.zcode', 'v2', 'bots-model-cache.v2.json'),
93
95
  }
94
96
  }
95
97
 
@@ -560,6 +562,137 @@ function writeForgeCodeConfig(model, apiKey, baseUrl, providerKey, paths = getDe
560
562
  return { filePath, backupPath }
561
563
  }
562
564
 
565
+ // ๐Ÿ“– writeZCodeConfig: Writes provider + model into ZCode's config.json and
566
+ // ๐Ÿ“– bots-model-cache.v2.json so the selected model appears in ZCode's model picker.
567
+ // ๐Ÿ“– Uses a deterministic provider ID (fcm-{providerKey}) and merges models so
568
+ // ๐Ÿ“– re-running on the same provider/model is idempotent โ€” no duplicates.
569
+ export function writeZCodeConfig(model, config, paths = getDefaultToolPaths()) {
570
+ const configPath = paths.zcodeConfigPath
571
+ const cachePath = paths.zcodeModelCachePath
572
+
573
+ const providerKey = model.providerKey || 'nvidia'
574
+ const apiKey = getApiKey(config, providerKey)
575
+ const baseUrl = sources[providerKey]?.url
576
+ ? sources[providerKey].url.replace(/\/chat\/completions$/i, '').replace(/\/responses$/i, '').replace(/\/predictions$/i, '')
577
+ : null
578
+ const providerId = `fcm-${providerKey}`
579
+ const providerLabel = `FCM ${sources[providerKey]?.name || providerKey}`
580
+
581
+ if (!baseUrl) {
582
+ throw new Error(`Cannot resolve base URL for ${sources[providerKey]?.name || providerKey}`)
583
+ }
584
+
585
+ const ctx = parseCtxToTokens(model.ctx) || 200000
586
+ const maxOutput = Math.max(4096, Math.min(ctx, 32768))
587
+
588
+ // โ”€โ”€ 1. Write to config.json โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
589
+ const cfg = readJson(configPath, { $schema: 'https://opencode.ai/config.json' })
590
+ if (!cfg.provider || typeof cfg.provider !== 'object') cfg.provider = {}
591
+
592
+ const existingProvider = cfg.provider[providerId]
593
+ let configModified = false
594
+
595
+ // ๐Ÿ“– If the provider already exists with this model โ€” skip (idempotent)
596
+ if (existingProvider && existingProvider.models?.[model.modelId]) {
597
+ // Model already configured โ€” skip config write, but still check cache
598
+ } else if (existingProvider) {
599
+ // ๐Ÿ“– Provider exists but model is new โ€” add model to existing provider
600
+ existingProvider.models[model.modelId] = {
601
+ limit: { context: ctx },
602
+ modalities: { input: ['text'], output: ['text'] },
603
+ }
604
+ if (ctx > 8192) {
605
+ existingProvider.models[model.modelId].limit.output = maxOutput
606
+ }
607
+ configModified = true
608
+ } else {
609
+ // ๐Ÿ“– New provider โ€” create it
610
+ cfg.provider[providerId] = {
611
+ name: providerLabel,
612
+ kind: 'openai-compatible',
613
+ options: {
614
+ apiKey: apiKey || '',
615
+ baseURL: baseUrl.replace(/\/v1\/chat\/completions$/, '').replace(/\/v1$/, '') + '/v1',
616
+ apiKeyRequired: true,
617
+ },
618
+ enabled: true,
619
+ source: 'custom',
620
+ models: {
621
+ [model.modelId]: {
622
+ limit: { context: ctx },
623
+ modalities: { input: ['text'], output: ['text'] },
624
+ },
625
+ },
626
+ }
627
+ if (ctx > 8192) {
628
+ cfg.provider[providerId].models[model.modelId].limit.output = maxOutput
629
+ }
630
+ configModified = true
631
+ }
632
+
633
+ const configBackupPath = configModified ? writeJson(configPath, cfg) : null
634
+
635
+ // โ”€โ”€ 2. Write to bots-model-cache.v2.json โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
636
+ const cache = readJson(cachePath, { version: 2, updatedAt: Date.now(), providers: [] })
637
+ if (!Array.isArray(cache.providers)) cache.providers = []
638
+
639
+ const existingCacheIdx = cache.providers.findIndex((p) => p?.id === providerId)
640
+ const modelAlreadyCached = existingCacheIdx >= 0
641
+ && cache.providers[existingCacheIdx].models?.some((m) => m?.id === model.modelId)
642
+
643
+ let cacheModified = false
644
+
645
+ if (!modelAlreadyCached) {
646
+ const modelEntry = {
647
+ id: model.modelId,
648
+ name: model.label || model.modelId,
649
+ kinds: ['openai-compatible'],
650
+ defaultKind: 'openai-compatible',
651
+ modalities: { input: ['text'], output: ['text'] },
652
+ contextWindow: ctx,
653
+ ...(ctx > 8192 ? { maxOutputTokens: maxOutput } : {}),
654
+ }
655
+
656
+ if (existingCacheIdx >= 0) {
657
+ // ๐Ÿ“– Provider exists in cache โ€” add model to existing entry
658
+ cache.providers[existingCacheIdx].models.push(modelEntry)
659
+ cache.providers[existingCacheIdx].updatedAt = Date.now()
660
+ } else {
661
+ // ๐Ÿ“– New provider in cache
662
+ cache.providers.push({
663
+ id: providerId,
664
+ name: providerLabel,
665
+ enabled: true,
666
+ endpoints: {
667
+ baseURL: baseUrl.replace(/\/v1\/chat\/completions$/, '').replace(/\/v1$/, '') + '/v1',
668
+ paths: { 'openai-compatible': '/chat/completions' },
669
+ },
670
+ apiFormat: 'openai-chat-completions',
671
+ source: 'custom',
672
+ apiKeyRequired: true,
673
+ apiKey: apiKey ? '__zcode_cached_api_key_present__' : '',
674
+ defaultKind: 'openai-compatible',
675
+ models: [modelEntry],
676
+ createdAt: Date.now(),
677
+ updatedAt: Date.now(),
678
+ })
679
+ }
680
+ cache.updatedAt = Date.now()
681
+ cacheModified = true
682
+ }
683
+
684
+ const cacheBackupPath = cacheModified ? writeJson(cachePath, cache) : null
685
+
686
+ return {
687
+ filePath: configPath,
688
+ backupPath: configBackupPath,
689
+ providerId,
690
+ cachePath,
691
+ cacheBackupPath,
692
+ skipped: !configModified && !cacheModified,
693
+ }
694
+ }
695
+
563
696
  // ๐Ÿ“– restartHermesGateway โ€” restart the Hermes messaging gateway after config changes.
564
697
  // ๐Ÿ“– Non-blocking: if gateway is not running, this is a no-op.
565
698
  function restartHermesGateway() {
@@ -829,18 +962,21 @@ export function prepareExternalToolLaunch(mode, model, config, options = {}) {
829
962
  }
830
963
 
831
964
  if (mode === 'zcode') {
832
- // ๐Ÿ“– ZCode is a desktop app from z.ai. Launch via `open -a ZCode` on macOS.
833
- // ๐Ÿ“– On other platforms the user is expected to have ZCode already running โ€” the
834
- // ๐Ÿ“– startExternalTool hook below prints manual setup steps and skips spawning.
965
+ // ๐Ÿ“– Write provider + model into ZCode config so it appears in ZCode's model picker.
966
+ const result = writeZCodeConfig(model, config, paths)
835
967
  const isMac = process.platform === 'darwin'
968
+
836
969
  return {
837
- command: isMac ? 'open' : 'true', // no-op on non-mac
838
- args: isMac ? ['-a', 'ZCode'] : [],
970
+ command: isMac ? 'open' : 'node',
971
+ args: isMac ? ['-a', 'ZCode'] : ['-e', 'process.exit(0)'],
839
972
  env,
840
973
  apiKey,
841
974
  baseUrl,
842
975
  meta,
843
- configArtifacts: [],
976
+ configArtifacts: [
977
+ { path: result.filePath, backupPath: result.backupPath, label: 'config' },
978
+ ...(result.cachePath ? [{ path: result.cachePath, backupPath: result.cacheBackupPath, label: 'model cache' }] : []),
979
+ ],
844
980
  }
845
981
  }
846
982
 
@@ -982,22 +1118,10 @@ export async function startExternalTool(mode, model, config) {
982
1118
  console.log(chalk.dim(` ๐Ÿ“– Attempting to launch Xcode...`))
983
1119
  }
984
1120
  if (mode === 'zcode') {
985
- // ๐Ÿ“– ZCode has UI-based config (Settings -> Model Settings / ๆจกๅž‹่ฎพ็ฝฎ). We point
986
- // ๐Ÿ“– the user at the FCM router (OpenAI-compatible) so they get free failover +
987
- // ๐Ÿ“– the per-provider normalizer on top of any FCM-tracked model.
988
- const routerBase = 'http://localhost:19280/v1'
989
- console.log(chalk.bold.cyan('\n ๐ŸงŠ ZCode Setup Instructions:'))
990
- console.log(chalk.white(' 1. Open ZCode and click the model selector in the chat input.'))
991
- console.log(chalk.white(' 2. At the bottom of the list, click ') + chalk.bold('Manage Models') + chalk.white(' (็ฎก็†ๆจกๅž‹)'))
992
- console.log(chalk.white(' 3. Click ') + chalk.bold('Add Provider') + chalk.white(' (ๆทปๅŠ ไพ›ๅบ”ๅ•†) in the left sidebar.'))
993
- console.log(chalk.white(' 4. Fill in the following details:'))
994
- console.log(chalk.dim(' Name: ') + chalk.green(`FCM Router`))
995
- console.log(chalk.dim(' Base URL: ') + chalk.green(routerBase))
996
- console.log(chalk.dim(' API Key: ') + chalk.green('fcm-local'))
997
- console.log(chalk.dim(' Protocol: ') + chalk.green('OpenAI-compatible'))
998
- console.log(chalk.white(' 5. Save, then pick ') + chalk.bold(`fcm`) + chalk.white(' from the model list. FCM picks the best live model.'))
999
- console.log(chalk.dim(` ๐Ÿ“– If you prefer a direct provider, use the URL/key for ${chalk.bold(sources[model.providerKey]?.name || model.providerKey)} shown above.\n`))
1000
- console.log(chalk.dim(` ๐Ÿ“– Attempting to launch ZCode...`))
1121
+ // ๐Ÿ“– ZCode is a desktop app โ€” config was already written by prepareExternalToolLaunch.
1122
+ // ๐Ÿ“– No process to spawn; the TUI stays open.
1123
+ console.log(chalk.dim(` ๐Ÿ“– ZCode config updated with model: ${model.modelId}. Open ZCode and select the model from the picker.`))
1124
+ return 0
1001
1125
  }
1002
1126
  if (mode === 'crush') console.log(chalk.dim(' ๐Ÿ“– Crush will use the provider directly for this launch.'))
1003
1127
 
@@ -1012,11 +1136,10 @@ export async function startExternalTool(mode, model, config) {
1012
1136
  jcode: ' ๐Ÿ“– Launching jcode...',
1013
1137
  copilot: ` ๐Ÿ“– Copilot CLI configured with model: ${model.modelId}`,
1014
1138
  forgecode: ` ๐Ÿ“– ForgeCode configured with model: ${model.modelId}`,
1015
- zcode: ` ๐Ÿ“– ZCode is a desktop app โ€” setup instructions printed below.`,
1016
1139
  }
1017
1140
  if (infoMessages[mode]) console.log(chalk.dim(infoMessages[mode]))
1018
1141
 
1019
- // ๐Ÿ“– xcode and zcode use raw command ("open"), everything else resolves via tool-bootstrap
1020
- const command = (mode === 'xcode' || mode === 'zcode') ? launchPlan.command : resolveLaunchCommand(mode, launchPlan.command)
1142
+ // ๐Ÿ“– xcode uses raw command ("open"), everything else resolves via tool-bootstrap
1143
+ const command = (mode === 'xcode') ? launchPlan.command : resolveLaunchCommand(mode, launchPlan.command)
1021
1144
  return spawnCommand(command, launchPlan.args, launchPlan.env)
1022
1145
  }
package/src/core/utils.js CHANGED
@@ -447,7 +447,7 @@ export function findBestModel(results) {
447
447
  // - Boolean flags: --best, --fiable, --opencode, --opencode-desktop, --opencode-web, --openclaw,
448
448
  // --aider, --crush, --goose, --qwen, --kilo,
449
449
  // --openhands, --amp, --pi, --hermes, --continue, --cline,
450
- // --xcode, --jcode, --copilot, --forgecode,
450
+ // --xcode, --jcode, --copilot, --forgecode, --zcode,
451
451
  // --daemon, --daemon-bg, --daemon-stop,
452
452
  // --daemon-status, --no-telemetry, --json, --help/-h (case-insensitive)
453
453
  // --playground / playground subcommand (open the in-TUI chat playground)
@@ -456,7 +456,7 @@ export function findBestModel(results) {
456
456
  // Returns:
457
457
  // { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openCodeWebMode, openClawMode,
458
458
  // aiderMode, crushMode, gooseMode, qwenMode, openHandsMode, ampMode,
459
- // piMode, jcodeMode, copilotMode, forgecodeMode, noTelemetry, jsonMode, helpMode, tierFilter }
459
+ // piMode, jcodeMode, copilotMode, forgecodeMode, zcodeMode, noTelemetry, jsonMode, helpMode, tierFilter }
460
460
  //
461
461
  // ๐Ÿ“– Note: apiKey may be null here โ€” the main CLI falls back to env vars and saved config.
462
462
  export function parseArgs(argv) {
@@ -534,6 +534,7 @@ export function parseArgs(argv) {
534
534
  const jcodeMode = flags.includes('--jcode')
535
535
  const copilotMode = flags.includes('--copilot')
536
536
  const forgecodeMode = flags.includes('--forgecode')
537
+ const zcodeMode = flags.includes('--zcode')
537
538
  const noTelemetry = flags.includes('--no-telemetry')
538
539
  const devMode = flags.includes('--dev')
539
540
  const jsonMode = flags.includes('--json')
@@ -597,6 +598,7 @@ export function parseArgs(argv) {
597
598
  jcodeMode,
598
599
  copilotMode,
599
600
  forgecodeMode,
601
+ zcodeMode,
600
602
  noTelemetry,
601
603
  jsonMode,
602
604
  helpMode,
package/src/tui/app.js CHANGED
@@ -250,10 +250,11 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
250
250
  cline: cliArgs.clineMode,
251
251
  xcode: cliArgs.xcodeMode,
252
252
  pi: cliArgs.piMode,
253
- caveman: cliArgs.cavemanMode,
254
- copilot: cliArgs.copilotMode,
255
- forgecode: cliArgs.forgecodeMode,
256
- }
253
+ caveman: cliArgs.cavemanMode,
254
+ copilot: cliArgs.copilotMode,
255
+ forgecode: cliArgs.forgecodeMode,
256
+ zcode: cliArgs.zcodeMode,
257
+ }
257
258
  return flagByMode[toolMode] === true
258
259
  })
259
260
  if (requestedMode) mode = requestedMode
@@ -181,12 +181,13 @@ export function createKeyHandler(ctx) {
181
181
  state.toolInstallPromptErrorMsg = null
182
182
  }
183
183
 
184
- function shouldCheckMissingTool(mode) {
185
- // ๐Ÿ“– opencode-desktop doesn't have a binary check (it uses 'open -a').
186
- // ๐Ÿ“– opencode-web, opencode, and kilo manage their own ENOENT errors in spawn handlers.
187
- // ๐Ÿ“– xcode uses 'open -a Xcode' which doesn't need a binary path resolution.
188
- return !['opencode-desktop', 'opencode-web', 'opencode', 'kilo', 'xcode'].includes(mode)
189
- }
184
+ function shouldCheckMissingTool(mode) {
185
+ // ๐Ÿ“– opencode-desktop doesn't have a binary check (it uses 'open -a').
186
+ // ๐Ÿ“– opencode-web, opencode, and kilo manage their own ENOENT errors in spawn handlers.
187
+ // ๐Ÿ“– xcode uses 'open -a Xcode' which doesn't need a binary path resolution.
188
+ // ๐Ÿ“– zcode is a desktop app with no CLI binary โ€” the launch handler prints setup instructions.
189
+ return !['opencode-desktop', 'opencode-web', 'opencode', 'kilo', 'xcode', 'zcode'].includes(mode)
190
+ }
190
191
 
191
192
  function getModelTelemetryFamily(providerKey) {
192
193
  if (providerKey === 'opencode-zen') return providerKey