coding-tool-x 3.8.9 → 3.9.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.
Files changed (130) hide show
  1. package/README.md +59 -387
  2. package/dist/web/assets/AgentsDrawer-DooV5Qhx.js +1 -0
  3. package/dist/web/assets/Analytics-BXRwqC-P.js +1 -0
  4. package/dist/web/assets/AssetPathField-B-LHvOxq.js +1 -0
  5. package/dist/web/assets/{ChatHistoryDrawer-BTmQ1aQr.js → ChatHistoryDrawer-CoHlGAxb.js} +1 -1
  6. package/dist/web/assets/{CollapseItem-CwqJk3bE.js → CollapseItem-kJkiHxFl.js} +1 -1
  7. package/dist/web/assets/CommandsDrawer-DLQrikcw.js +1 -0
  8. package/dist/web/assets/ConfigExportDrawer-BItwo0FQ.js +1 -0
  9. package/dist/web/assets/{ConfigTemplates-gyERg9iC.js → ConfigTemplates-R3YzzBT3.js} +1 -1
  10. package/dist/web/assets/{ConfigTemplatesDrawer-B17RqFi4.js → ConfigTemplatesDrawer-C_nPKrhx.js} +1 -1
  11. package/dist/web/assets/ConfigTemplatesPanel-BiwrNyYN.js +1 -0
  12. package/dist/web/assets/{Divider-DsdbkHrG.js → Divider-BLLhMTSt.js} +1 -1
  13. package/dist/web/assets/FavoritesDrawer-BAJ-AOpN.js +1 -0
  14. package/dist/web/assets/GatewayConvertDrawer-Av3yzwhP.js +1 -0
  15. package/dist/web/assets/Home-B01aL_SR.js +1 -0
  16. package/dist/web/assets/{MarkdownEditor-9TTwTS-U.js → MarkdownEditor-D75GGt5X.js} +1 -1
  17. package/dist/web/assets/McpDrawer-xNzJtT4X.js +1 -0
  18. package/dist/web/assets/McpFormDrawer-B240mJ5y.js +1 -0
  19. package/dist/web/assets/{PluginManager-BuRcWuBj.js → PluginManager-CgLjBdlr.js} +1 -1
  20. package/dist/web/assets/{PluginsDrawer-_zZENaQn.js → PluginsDrawer-DvJdwMXx.js} +1 -1
  21. package/dist/web/assets/PluginsPanel-kX14E_DT.js +1 -0
  22. package/dist/web/assets/ProjectList-CRtc3Hhx.js +1 -0
  23. package/dist/web/assets/PromptsDrawer-CvT9dA2N.js +1 -0
  24. package/dist/web/assets/RecentSessionsDrawer-DM1gf584.js +1 -0
  25. package/dist/web/assets/SessionCard-cZR2V4Br.js +1 -0
  26. package/dist/web/assets/SessionList-D7-P4Zme.js +1 -0
  27. package/dist/web/assets/SettingsDrawer-BFzaxbgC.css +1 -0
  28. package/dist/web/assets/SettingsDrawer-gW8kh-S8.js +1 -0
  29. package/dist/web/assets/{SkillManager-S698Hns8.js → SkillManager-D0BlI7CG.js} +1 -1
  30. package/dist/web/assets/{SkillsDrawer-Ms4lPCow.js → SkillsDrawer-BMB6uzqP.js} +1 -1
  31. package/dist/web/assets/SkillsPanel-BzR828De.js +1 -0
  32. package/dist/web/assets/Upload-3fNyOQrx.js +1 -0
  33. package/dist/web/assets/{WorkspaceDrawer-L7HJRc6u.js → WorkspaceDrawer-DvtBA58Q.js} +1 -1
  34. package/dist/web/assets/{WorkspaceManager-DsoNg2z8.js → WorkspaceManager-BW5KtsHO.js} +1 -1
  35. package/dist/web/assets/WorkspacePanel-BC_xpAEZ.js +1 -0
  36. package/dist/web/assets/client-BgcNg4Bt.js +1 -0
  37. package/dist/web/assets/{headers-B5JWGn3v.js → headers-DbowkmZJ.js} +1 -1
  38. package/dist/web/assets/{icons-DmFe4b5j.js → icons-B-AzZoeE.js} +1 -1
  39. package/dist/web/assets/index-BFqnddoI.js +2 -0
  40. package/dist/web/assets/index-Bn8BUnwP.css +1 -0
  41. package/dist/web/assets/{light-0eavxNvt.js → light-C2X7hegj.js} +1 -1
  42. package/dist/web/assets/platforms-DrzAtnjY.js +1 -0
  43. package/dist/web/assets/platforms-mqqe_82d.js +1 -0
  44. package/dist/web/assets/project-config-BVt5s-pH.js +1 -0
  45. package/dist/web/assets/sessions-53-Fr68H.js +1 -0
  46. package/dist/web/assets/{sessions-D4yQtfKb.js → sessions-CaWUaysM.js} +1 -1
  47. package/dist/web/assets/use-message-DBsCm7FJ.js +1 -0
  48. package/dist/web/assets/{useResponsiveDrawer-V_onl9NV.js → useResponsiveDrawer-Ckgj760z.js} +1 -1
  49. package/dist/web/assets/{workspaces-mjyPVGS8.js → workspaces-DQuZd4qw.js} +1 -1
  50. package/dist/web/index.html +8 -8
  51. package/docs/analytics.png +0 -0
  52. package/docs/home.png +0 -0
  53. package/package.json +4 -2
  54. package/src/commands/export-config.js +17 -211
  55. package/src/config/model-metadata.json +180 -16
  56. package/src/platforms/channel-auth-service.js +1 -1
  57. package/src/platforms/commands-service.js +28 -0
  58. package/src/platforms/driver-registry.js +1 -0
  59. package/src/platforms/drivers/claude/channels-implementation.js +79 -90
  60. package/src/platforms/drivers/claude/native-config-implementation.js +3 -4
  61. package/src/platforms/drivers/codex/channels-implementation.js +34 -49
  62. package/src/platforms/drivers/codex/native-config-implementation.js +3 -51
  63. package/src/platforms/drivers/dsh/api-operations.js +1 -1
  64. package/src/platforms/drivers/dsh/channels-implementation.js +28 -8
  65. package/src/platforms/drivers/dsh/channels.js +0 -18
  66. package/src/platforms/drivers/dsh/common.js +7 -0
  67. package/src/platforms/drivers/dsh/native-config.js +7 -2
  68. package/src/platforms/drivers/gemini/channels-implementation.js +47 -55
  69. package/src/platforms/drivers/gemini/native-config-implementation.js +8 -20
  70. package/src/platforms/drivers/generic-native-config.js +20 -0
  71. package/src/platforms/drivers/native-config-snapshot.js +30 -4
  72. package/src/platforms/drivers/omp/channels-implementation.js +51 -17
  73. package/src/platforms/drivers/omp/native-config-implementation.js +10 -17
  74. package/src/platforms/drivers/opencode/channels-implementation.js +6 -0
  75. package/src/platforms/drivers/opencode/native-config-implementation.js +3 -67
  76. package/src/platforms/manifest-schema.js +35 -2
  77. package/src/platforms/manifests/claude.json +0 -8
  78. package/src/platforms/manifests/codex.json +0 -8
  79. package/src/platforms/manifests/dsh.json +6 -9
  80. package/src/platforms/manifests/gemini.json +0 -8
  81. package/src/platforms/manifests/omp.json +0 -8
  82. package/src/platforms/manifests/opencode.json +0 -8
  83. package/src/platforms/native-oauth-adapters.js +33 -33
  84. package/src/platforms/remote-notification-providers.js +5 -5
  85. package/src/server/api/settings.js +6 -5
  86. package/src/server/index.js +2 -0
  87. package/src/server/services/bounded-content.js +60 -0
  88. package/src/server/services/channel-balance.js +10 -4
  89. package/src/server/services/config-export-service.js +2 -2
  90. package/src/server/services/config-templates-service.js +5 -1
  91. package/src/server/services/plugins-service.js +162 -85
  92. package/src/server/services/session-history-maintenance-worker.js +71 -0
  93. package/src/server/services/session-history-maintenance.js +166 -0
  94. package/src/server/services/skill-service.js +23 -3
  95. package/src/server/services/speed-test.js +0 -72
  96. package/src/shared/channel-driver.js +16 -0
  97. package/src/shared/driver-factories/api.js +1 -0
  98. package/src/utils/native-config-patcher.js +474 -0
  99. package/dist/web/assets/AgentsDrawer-Cj5CJ4d9.js +0 -1
  100. package/dist/web/assets/Analytics-1doS-8uC.js +0 -1
  101. package/dist/web/assets/AssetPathField-C9TOPl7o.js +0 -1
  102. package/dist/web/assets/CommandsDrawer-CaZWDI_G.js +0 -1
  103. package/dist/web/assets/ConfigExportDrawer-C3vSZEsS.js +0 -1
  104. package/dist/web/assets/ConfigTemplatesPanel-DKIC_FqG.js +0 -1
  105. package/dist/web/assets/FavoritesDrawer-DaziQmBp.js +0 -1
  106. package/dist/web/assets/GatewayConvertDrawer-OMgWUUSc.js +0 -1
  107. package/dist/web/assets/Home-DOSUTfrE.js +0 -1
  108. package/dist/web/assets/McpDrawer-I8rl8lGH.js +0 -1
  109. package/dist/web/assets/McpFormDrawer-DEP3C7ut.js +0 -1
  110. package/dist/web/assets/PluginsPanel-Bs5qZ-TK.js +0 -1
  111. package/dist/web/assets/ProjectList-BSxuL2td.js +0 -1
  112. package/dist/web/assets/PromptsDrawer-CoZ-ZtsY.js +0 -1
  113. package/dist/web/assets/RecentSessionsDrawer-B_17aJBG.js +0 -1
  114. package/dist/web/assets/SessionCard-BL_x0d3t.js +0 -1
  115. package/dist/web/assets/SessionList-DhRYZ6vl.js +0 -1
  116. package/dist/web/assets/SettingsDrawer-Dw5NUgHC.css +0 -1
  117. package/dist/web/assets/SettingsDrawer-fYb0DU7X.js +0 -1
  118. package/dist/web/assets/SkillsPanel-rqnqAOGL.js +0 -1
  119. package/dist/web/assets/SpeedTestDrawer-Bbn38SfV.js +0 -1
  120. package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +0 -1
  121. package/dist/web/assets/Upload-CdyVHxCI.js +0 -1
  122. package/dist/web/assets/WorkspacePanel-Be8PVZah.js +0 -1
  123. package/dist/web/assets/client-UDwXBP9i.js +0 -1
  124. package/dist/web/assets/index-3Lp6z8TQ.css +0 -1
  125. package/dist/web/assets/index-BKlAWTH-.js +0 -2
  126. package/dist/web/assets/platforms-BmzzwnSw.js +0 -1
  127. package/dist/web/assets/platforms-DWXgmy2w.js +0 -1
  128. package/dist/web/assets/project-config-B2baPzYL.js +0 -1
  129. package/dist/web/assets/sessions-CC0j-KH_.js +0 -1
  130. package/dist/web/assets/use-message-DqCwefG6.js +0 -1
@@ -83,17 +83,43 @@ function writeEntry(spec, entry, overwrite) {
83
83
  return 'success';
84
84
  }
85
85
 
86
- function createNativeSnapshotMethods(specs, { platform, runtime } = {}) {
86
+ function resolveSnapshotPath(spec, { pathContext, paths, manifest } = {}) {
87
+ const rawPath = typeof spec?.path === 'string' ? spec.path.trim() : '';
88
+ if (!rawPath) return null;
89
+
90
+ const nativePaths = pathContext?.native || paths || {};
91
+ if (typeof nativePaths[rawPath] === 'string' && nativePaths[rawPath].trim()) {
92
+ return nativePaths[rawPath];
93
+ }
94
+
95
+ const home = pathContext?.home || nativePaths.home || manifest?.paths?.home || process.cwd();
96
+ const expanded = rawPath.replace(/\{home\}/g, home);
97
+ return path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(home, expanded);
98
+ }
99
+
100
+ function resolveNativeSnapshotSpecs(declarations, context = {}) {
101
+ return Object.fromEntries(Object.entries(declarations || {})
102
+ .map(([key, spec]) => [key, {
103
+ ...spec,
104
+ path: resolveSnapshotPath(spec, context)
105
+ }])
106
+ .filter(([, spec]) => spec.path));
107
+ }
108
+
109
+ function createNativeSnapshotMethods(specs, { platform, runtime, manifest, pathContext, paths } = {}) {
110
+ const resolvedSpecs = Object.keys(specs || {}).length > 0
111
+ ? specs
112
+ : resolveNativeSnapshotSpecs(manifest?.nativeConfigSnapshot, { manifest, pathContext, paths });
87
113
  const methods = {
88
114
  exportSnapshot() {
89
- return Object.fromEntries(Object.entries(specs || {})
115
+ return Object.fromEntries(Object.entries(resolvedSpecs || {})
90
116
  .map(([key, spec]) => [key, readEntry(spec)])
91
117
  .filter(([, entry]) => entry));
92
118
  },
93
119
  importSnapshot(snapshot = {}, { overwrite = true } = {}) {
94
120
  const result = { success: 0, imported: 0, skipped: 0, failed: 0 };
95
121
  for (const [key, entry] of Object.entries(snapshot || {})) {
96
- const status = writeEntry(specs[key], entry, overwrite);
122
+ const status = writeEntry(resolvedSpecs[key], entry, overwrite);
97
123
  if (status === 'success') {
98
124
  result.success += 1;
99
125
  result.imported += 1;
@@ -122,4 +148,4 @@ function createNativeSnapshotMethods(specs, { platform, runtime } = {}) {
122
148
  return methods;
123
149
  }
124
150
 
125
- module.exports = { createNativeSnapshotMethods };
151
+ module.exports = { createNativeSnapshotMethods, resolveNativeSnapshotSpecs };
@@ -10,6 +10,7 @@ const {
10
10
  writeManagedOmpProviders,
11
11
  removeManagedOmpProviders,
12
12
  getLastManagedOmpSyncResult,
13
+ getManagedProviderId,
13
14
  readModelsConfig,
14
15
  readOmpSettingsConfig,
15
16
  normalizeProviderId,
@@ -59,6 +60,15 @@ function selectLatestEnabledChannel(channels = []) {
59
60
  }, enabledChannels[0]);
60
61
  }
61
62
 
63
+ function getChannelManagedProviderId(channel = {}) {
64
+ if (typeof getManagedProviderId === 'function') {
65
+ return getManagedProviderId(channel);
66
+ }
67
+ const rawId = channel.managedProviderId || channel.providerKey || channel.provider || channel.name || channel.id;
68
+ const normalizedId = normalizeProviderId(rawId);
69
+ return normalizedId.startsWith('ctx-') ? normalizedId : `ctx-${normalizedId}`;
70
+ }
71
+
62
72
  function loadManagedOmpModeState() {
63
73
  ensureStorageDirMigrated();
64
74
  try {
@@ -269,16 +279,24 @@ class OmpChannelService extends BaseChannelService {
269
279
  }
270
280
 
271
281
  _applyToNativeSettings(channel) {
282
+ const knownProviderIds = this.getChannels().channels.map(getChannelManagedProviderId);
272
283
  if (isManagedOmpModeEnabled()) {
273
284
  const state = loadManagedOmpModeState();
274
285
  enableManagedOmpMode(channel.id, state?.gateway || null);
275
286
  writeManagedOmpProviders([channel], state?.gateway ? {
276
287
  gateway: state.gateway,
277
- activeChannelId: channel.id
278
- } : {});
288
+ activeChannelId: channel.id,
289
+ knownProviderIds
290
+ } : { knownProviderIds });
279
291
  return;
280
292
  }
281
- writeManagedOmpProviders([channel], {});
293
+ writeManagedOmpProviders([channel], { knownProviderIds });
294
+ }
295
+
296
+ disableAllChannels() {
297
+ const result = super.disableAllChannels();
298
+ this.syncOmpProvidersForCurrentMode(this.getChannels().channels);
299
+ return result;
282
300
  }
283
301
 
284
302
  _onAfterCreate(_channel, allChannels) {
@@ -296,13 +314,18 @@ class OmpChannelService extends BaseChannelService {
296
314
  syncOmpProvidersForCurrentMode(channels = this.getChannels().channels) {
297
315
  if (isManagedOmpModeEnabled()) {
298
316
  const state = loadManagedOmpModeState();
299
- return this.syncManagedOmpProviders(channels, state?.gateway ? {
300
- gateway: state.gateway,
301
- activeChannelId: state.activeChannelId
302
- } : {});
317
+ return this.syncManagedOmpProviders(channels, {
318
+ ...(state?.gateway ? {
319
+ gateway: state.gateway,
320
+ activeChannelId: state.activeChannelId
321
+ } : {}),
322
+ knownProviderIds: channels.map(getChannelManagedProviderId)
323
+ });
303
324
  }
304
325
  const activeChannel = selectLatestEnabledChannel(channels);
305
- return this.syncManagedOmpProviders(activeChannel ? [activeChannel] : [], {});
326
+ return this.syncManagedOmpProviders(activeChannel ? [activeChannel] : [], {
327
+ knownProviderIds: channels.map(getChannelManagedProviderId)
328
+ });
306
329
  }
307
330
 
308
331
  syncManagedOmpProviders(channels = this.getChannels().channels, options = {}) {
@@ -335,7 +358,9 @@ class OmpChannelService extends BaseChannelService {
335
358
 
336
359
  try {
337
360
  this.saveChannels(data);
338
- const sync = this.syncManagedOmpProviders([channel], {});
361
+ const sync = this.syncManagedOmpProviders([channel], {
362
+ knownProviderIds: data.channels.map(getChannelManagedProviderId)
363
+ });
339
364
  if (!wasEnabled) {
340
365
  clearOmpChannelBalanceCache(channel);
341
366
  }
@@ -739,24 +764,33 @@ function getCatalogMetadata({
739
764
  ))
740
765
  : [])
741
766
  ]
742
- .map(value => String(value || '').trim().toLowerCase())
743
- .filter(Boolean);
767
+ .map(value => String(value || '').trim())
768
+ .filter(Boolean)
769
+ .filter((value, index, values) => values.findIndex(item => item.toLowerCase() === value.toLowerCase()) === index);
744
770
  const normalizedProviderKey = String(providerKey || '').trim().toLowerCase();
745
771
 
746
772
  const allEntries = Object.entries(MODEL_METADATA)
747
773
  .filter(([, metadata]) => Array.isArray(metadata.toolTypes) && metadata.toolTypes.includes('omp'))
748
774
  .map(([id, metadata]) => ({ ...structuredClone(metadata), id }));
749
775
 
750
- const matchesRequestedId = entry => requestedIds.length > 0 && requestedIds.some(requested => (
751
- requested === entry.id.toLowerCase()
752
- || requested === String(entry.sourceId || '').toLowerCase()
753
- || requested === entry.id.split('/').pop().toLowerCase()
754
- ));
776
+ const findRequestedEntry = requestedId => allEntries.find(entry => {
777
+ const normalizedRequestedId = requestedId.toLowerCase();
778
+ return normalizedRequestedId === entry.id.toLowerCase()
779
+ || normalizedRequestedId === String(entry.sourceId || '').toLowerCase()
780
+ || normalizedRequestedId === entry.id.split('/').pop().toLowerCase();
781
+ });
755
782
  const matchesProvider = entry => (
756
783
  String(entry.provider || '').toLowerCase() === normalizedProviderKey
757
784
  || String(entry.sourceId || '').toLowerCase().startsWith(`${normalizedProviderKey}/`)
758
785
  );
759
- const requestedEntries = requestedIds.length > 0 ? allEntries.filter(matchesRequestedId) : [];
786
+ const requestedEntries = requestedIds.length > 0
787
+ ? requestedIds
788
+ .map(requestedId => {
789
+ const entry = findRequestedEntry(requestedId);
790
+ return entry ? { ...entry, id: requestedId } : null;
791
+ })
792
+ .filter(Boolean)
793
+ : [];
760
794
  const providerEntries = normalizedProviderKey ? allEntries.filter(matchesProvider) : [];
761
795
  const entries = requestedIds.length > 0 ? requestedEntries : providerEntries;
762
796
 
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { spawnSync } = require('child_process');
4
4
  const yaml = require('js-yaml');
5
+ const { writeYamlFile } = require('../../../utils/native-config-patcher');
5
6
  const ompConfig = require('./config');
6
7
  const { prepareManagedOmpChannels } = require('./gateway-routing');
7
8
  const { MODEL_METADATA, MODEL_ALIASES } = require('../../../config/model-metadata');
@@ -409,13 +410,7 @@ function readModelsConfig(filePath = getOmpPaths().modelsYml) {
409
410
  }
410
411
 
411
412
  function writeModelsConfig(config, filePath = getOmpPaths().modelsYml) {
412
- ensureOmpDir(path.dirname(filePath));
413
- const doc = yaml.dump(config || { providers: {} }, {
414
- lineWidth: 120,
415
- noRefs: true,
416
- sortKeys: false
417
- });
418
- writeFileAtomic(filePath, doc);
413
+ writeYamlFile(filePath, config || { providers: {} }, { atomic: true });
419
414
  }
420
415
 
421
416
  function getOmpSettingsPath(paths = getOmpPaths()) {
@@ -447,13 +442,7 @@ function readOmpSettingsConfig(filePath = getOmpSettingsPath()) {
447
442
  }
448
443
 
449
444
  function writeOmpSettingsConfig(config, filePath = getOmpSettingsPath()) {
450
- ensureOmpDir(path.dirname(filePath));
451
- const doc = yaml.dump(config || {}, {
452
- lineWidth: 120,
453
- noRefs: true,
454
- sortKeys: false
455
- });
456
- writeFileAtomic(filePath, doc);
445
+ writeYamlFile(filePath, config || {}, { atomic: true });
457
446
  }
458
447
 
459
448
  function getModelsBackupPrefix(filePath) {
@@ -669,9 +658,10 @@ function removeLegacyManagedExtension(paths = getOmpPaths()) {
669
658
  }
670
659
  }
671
660
 
672
- function pruneManagedProviders(providers = {}) {
661
+ function pruneManagedProviders(providers = {}, knownProviderIds = null) {
662
+ const keep = knownProviderIds instanceof Set ? knownProviderIds : null;
673
663
  Object.keys(providers).forEach((providerId) => {
674
- if (isManagedProviderId(providerId)) {
664
+ if (isManagedProviderId(providerId) && (!keep || !keep.has(providerId))) {
675
665
  delete providers[providerId];
676
666
  }
677
667
  });
@@ -684,7 +674,10 @@ function buildManagedModelsConfig(channels = [], baseConfig = readModelsConfig()
684
674
  ...(baseConfig.providers || {})
685
675
  }
686
676
  };
687
- pruneManagedProviders(next.providers);
677
+ const knownProviderIds = Array.isArray(options.knownProviderIds)
678
+ ? new Set(options.knownProviderIds)
679
+ : new Set((channels || []).map(getManagedProviderId));
680
+ pruneManagedProviders(next.providers, knownProviderIds);
688
681
 
689
682
  channels
690
683
  .filter(channel => channel && channel.enabled !== false && channel.baseUrl)
@@ -282,6 +282,10 @@ function updateChannel(channelId, updates) {
282
282
  if (!isProxyRunning) {
283
283
  if (oldChannel.enabled === false && merged.enabled !== false) {
284
284
  syncManagedChannelConfig(data.channels, merged);
285
+ } else if (oldChannel.enabled !== false && merged.enabled === false) {
286
+ const activeChannel = resolveCurrentManagedChannel(data.channels);
287
+ if (activeChannel) syncManagedChannelConfig(data.channels, activeChannel);
288
+ else clearManagedChannelConfig();
285
289
  } else {
286
290
  const activeChannel = resolveCurrentManagedChannel(data.channels);
287
291
  if (merged.enabled !== false && activeChannel?.id === merged.id) {
@@ -487,9 +491,11 @@ async function getEffectiveApiKey(channel) {
487
491
  }
488
492
 
489
493
  function disableAllChannels() {
494
+ const proxyRunning = getOpenCodeProxyRunning();
490
495
  const data = loadChannels();
491
496
  data.channels.forEach(ch => { ch.enabled = false; });
492
497
  saveChannels(data);
498
+ if (!proxyRunning) clearManagedChannelConfig();
493
499
  }
494
500
 
495
501
  function getCurrentOpenCodeProviderId(config = {}) {
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { readJsoncFile, writeJsoncFile } = require('../../../utils/native-config-patcher');
3
4
  const { NATIVE_PATHS } = require('../../../config/paths');
4
5
  const { resolveModelMetadata } = require('../../../config/model-metadata');
5
6
  const { resolveModelPricing } = require('../../../server/utils/pricing');
@@ -61,79 +62,18 @@ function selectConfigPath() {
61
62
  return CONFIG_PATHS.opencode;
62
63
  }
63
64
 
64
- function stripJsonComments(input) {
65
- let result = '';
66
- let inString = false;
67
- let stringChar = '';
68
- let i = 0;
69
-
70
- while (i < input.length) {
71
- const ch = input[i];
72
- const next = input[i + 1];
73
-
74
- if (inString) {
75
- result += ch;
76
- if (ch === '\\') {
77
- if (next) {
78
- result += next;
79
- i += 2;
80
- continue;
81
- }
82
- } else if (ch === stringChar) {
83
- inString = false;
84
- }
85
- i += 1;
86
- continue;
87
- }
88
-
89
- if (ch === '"' || ch === '\'') {
90
- inString = true;
91
- stringChar = ch;
92
- result += ch;
93
- i += 1;
94
- continue;
95
- }
96
-
97
- if (ch === '/' && next === '/') {
98
- i += 2;
99
- while (i < input.length && input[i] !== '\n') i += 1;
100
- continue;
101
- }
102
-
103
- if (ch === '/' && next === '*') {
104
- i += 2;
105
- while (i < input.length - 1 && !(input[i] === '*' && input[i + 1] === '/')) i += 1;
106
- i += 2;
107
- continue;
108
- }
109
-
110
- result += ch;
111
- i += 1;
112
- }
113
-
114
- return result;
115
- }
116
-
117
65
  function readConfig(filePath) {
118
66
  if (!fs.existsSync(filePath)) return {};
119
67
 
120
- const raw = fs.readFileSync(filePath, 'utf8');
121
- if (!raw.trim()) return {};
122
-
123
68
  try {
124
- if (filePath.endsWith('.jsonc')) {
125
- return JSON.parse(stripJsonComments(raw));
126
- }
127
- return JSON.parse(raw);
69
+ return readJsoncFile(filePath);
128
70
  } catch (err) {
129
71
  throw new Error(`Failed to parse ${path.basename(filePath)}: ${err.message}`);
130
72
  }
131
73
  }
132
74
 
133
75
  function writeConfig(filePath, config) {
134
- ensureConfigDir();
135
- const content = JSON.stringify(config, null, 2);
136
- fs.writeFileSync(filePath, content, 'utf8');
76
+ writeJsoncFile(filePath, config);
137
77
  }
138
78
 
139
79
  function normalizeOpenCodeModel(modelId, providerId) {
@@ -520,7 +460,6 @@ function setProxyConfig(proxyPort, options = {}) {
520
460
 
521
461
  function setChannelConfig(channel = {}) {
522
462
  const filePath = selectConfigPath();
523
- backupConfig(filePath);
524
463
 
525
464
  const current = readConfig(filePath);
526
465
  const next = (current && typeof current === 'object') ? current : {};
@@ -529,9 +468,6 @@ function setChannelConfig(channel = {}) {
529
468
  next.provider = {};
530
469
  }
531
470
 
532
- const removedProviderKeys = clearManagedProviders(next);
533
- clearManagedModelRef(next, removedProviderKeys);
534
-
535
471
  const baseProviderKey = sanitizeProviderKey(channel.providerKey || channel.name || 'ctx-channel');
536
472
  let providerKey = baseProviderKey;
537
473
  let suffix = 2;
@@ -11,6 +11,7 @@ const DRIVER_IDS = new Set([
11
11
  'generic-openai-compatible',
12
12
  'generic-mcp',
13
13
  'generic-prompt',
14
+ 'generic-native-config',
14
15
  'legacy:claude',
15
16
  'legacy:codex',
16
17
  'legacy:gemini',
@@ -80,7 +81,7 @@ const CAPABILITY_OPERATIONS = Object.freeze({
80
81
  'updateSkill',
81
82
  'deleteSkill'
82
83
  ]),
83
- channels: new Set(['applyToSettings', 'bestForRestore', 'catalogMetadata', 'create', 'current', 'enabled', 'getAuth', 'getAuthQuota', 'list', 'models', 'order', 'poolStatus', 'probeModels', 'remove', 'resetHealth', 'speedTest', 'speedTestAll', 'sync', 'syncLocalAuth', 'update']),
84
+ channels: new Set(['applyToSettings', 'bestForRestore', 'catalogMetadata', 'create', 'current', 'enabled', 'getAuth', 'getAuthQuota', 'list', 'models', 'order', 'poolStatus', 'probeModels', 'remove', 'resetHealth', 'speedTest', 'sync', 'syncLocalAuth', 'update']),
84
85
  health: new Set(['healthCheck']),
85
86
  hooks: new Set(['getHooks', 'saveHooks', 'testHooks']),
86
87
  projects: new Set(['createProject', 'deleteProject', 'listProjects', 'saveProjectOrder']),
@@ -198,6 +199,19 @@ const schema = {
198
199
  sessionMapping: { type: 'object', additionalProperties: { type: 'string' } },
199
200
  sessionGlob: { type: 'string', minLength: 1 },
200
201
  resourceMappings: { type: 'object', additionalProperties: { type: 'string' } },
202
+ nativeConfigSnapshot: {
203
+ type: 'object',
204
+ additionalProperties: {
205
+ type: 'object',
206
+ additionalProperties: false,
207
+ required: ['path', 'format'],
208
+ properties: {
209
+ path: { type: 'string', minLength: 1 },
210
+ format: { enum: ['text', 'json', 'yaml', 'directory'] },
211
+ mode: { type: 'integer', minimum: 0, maximum: 511 }
212
+ }
213
+ }
214
+ },
201
215
  projectResources: {
202
216
  type: 'object',
203
217
  additionalProperties: false,
@@ -333,6 +347,17 @@ function validateProjectResources(manifest, errors) {
333
347
  }
334
348
  }
335
349
 
350
+ function validateNativeConfigSnapshot(manifest, errors) {
351
+ for (const [key, spec] of Object.entries(manifest?.nativeConfigSnapshot || {})) {
352
+ if (!isSafeRelativePath(spec?.path)) {
353
+ errors.push({
354
+ instancePath: `/nativeConfigSnapshot/${key}/path`,
355
+ message: 'must be a safe relative path or a declared native path name'
356
+ });
357
+ }
358
+ }
359
+ }
360
+
336
361
  function validateApiRoutes(manifest, errors) {
337
362
  const api = manifest?.api;
338
363
  if (!api?.routes) return;
@@ -395,7 +420,8 @@ function validateManifest(manifest) {
395
420
  'generic-filesystem': new Set(['resourceSync']),
396
421
  'generic-openai-compatible': new Set(['channels']),
397
422
  'generic-mcp': new Set(['mcp']),
398
- 'generic-prompt': new Set(['prompts'])
423
+ 'generic-prompt': new Set(['prompts']),
424
+ 'generic-native-config': new Set(['nativeConfig'])
399
425
  };
400
426
 
401
427
  for (const [capability, driver] of Object.entries(manifest && manifest.capabilities || {})) {
@@ -424,6 +450,13 @@ function validateManifest(manifest) {
424
450
  }
425
451
  }
426
452
 
453
+ if (manifest?.capabilities?.nativeConfig === 'generic-native-config' && !manifest.nativeConfigSnapshot) {
454
+ errors.push({
455
+ instancePath: '/nativeConfigSnapshot',
456
+ message: 'generic-native-config requires nativeConfigSnapshot declarations'
457
+ });
458
+ }
459
+ validateNativeConfigSnapshot(manifest, errors);
427
460
  validateApiRoutes(manifest, errors);
428
461
  return { valid: errors.length === 0, errors };
429
462
  }
@@ -356,14 +356,6 @@
356
356
  "request": "default",
357
357
  "response": "default"
358
358
  },
359
- {
360
- "method": "POST",
361
- "path": "/channels/speed-test-all",
362
- "capability": "channels",
363
- "operation": "speedTestAll",
364
- "request": "default",
365
- "response": "default"
366
- },
367
359
  {
368
360
  "method": "POST",
369
361
  "path": "/channels/probe-models",
@@ -348,14 +348,6 @@
348
348
  "request": "default",
349
349
  "response": "default"
350
350
  },
351
- {
352
- "method": "POST",
353
- "path": "/channels/speed-test-all",
354
- "capability": "channels",
355
- "operation": "speedTestAll",
356
- "request": "default",
357
- "response": "default"
358
- },
359
351
  {
360
352
  "method": "POST",
361
353
  "path": "/channels/probe-models",
@@ -253,14 +253,6 @@
253
253
  "request": "default",
254
254
  "response": "default"
255
255
  },
256
- {
257
- "method": "POST",
258
- "path": "/channels/speed-test-all",
259
- "capability": "channels",
260
- "operation": "speedTestAll",
261
- "request": "default",
262
- "response": "default"
263
- },
264
256
  {
265
257
  "method": "POST",
266
258
  "path": "/channels/probe-models",
@@ -530,7 +522,7 @@
530
522
  "capabilities": {
531
523
  "projects": "dsh-projects",
532
524
  "sessions": "dsh-sessions",
533
- "nativeConfig": "dsh-native-config",
525
+ "nativeConfig": "generic-native-config",
534
526
  "api": "dsh-api",
535
527
  "channels": "dsh-channels",
536
528
  "proxy": "dsh-proxy",
@@ -538,5 +530,10 @@
538
530
  "nativeLogs": "dsh-native-logs",
539
531
  "mcp": "dsh-api",
540
532
  "prompts": "dsh-api"
533
+ },
534
+ "nativeConfigSnapshot": {
535
+ "settings": { "path": "settings", "format": "text", "mode": 384 },
536
+ "credentials": { "path": "credentials", "format": "text", "mode": 384 },
537
+ "patch": { "path": "patch", "format": "text", "mode": 384 }
541
538
  }
542
539
  }
@@ -347,14 +347,6 @@
347
347
  "request": "default",
348
348
  "response": "default"
349
349
  },
350
- {
351
- "method": "POST",
352
- "path": "/channels/speed-test-all",
353
- "capability": "channels",
354
- "operation": "speedTestAll",
355
- "request": "default",
356
- "response": "default"
357
- },
358
350
  {
359
351
  "method": "POST",
360
352
  "path": "/channels/probe-models",
@@ -365,14 +365,6 @@
365
365
  "request": "default",
366
366
  "response": "default"
367
367
  },
368
- {
369
- "method": "POST",
370
- "path": "/channels/speed-test-all",
371
- "capability": "channels",
372
- "operation": "speedTestAll",
373
- "request": "default",
374
- "response": "default"
375
- },
376
368
  {
377
369
  "method": "POST",
378
370
  "path": "/channels/probe-models",
@@ -349,14 +349,6 @@
349
349
  "request": "default",
350
350
  "response": "default"
351
351
  },
352
- {
353
- "method": "POST",
354
- "path": "/channels/speed-test-all",
355
- "capability": "channels",
356
- "operation": "speedTestAll",
357
- "request": "default",
358
- "response": "default"
359
- },
360
352
  {
361
353
  "method": "POST",
362
354
  "path": "/channels/probe-models",
@@ -4,7 +4,7 @@ const os = require('os');
4
4
  const crypto = require('crypto');
5
5
  const { execFileSync, spawnSync } = require('child_process');
6
6
  const toml = require('toml');
7
- const tomlStringify = require('@iarna/toml').stringify;
7
+ const { writeTomlFile } = require('../utils/native-config-patcher');
8
8
  const pathsModule = require('../config/paths');
9
9
  const DEFAULT_NATIVE_PATHS = pathsModule.NATIVE_PATHS;
10
10
  const DEFAULT_PATHS = pathsModule.PATHS;
@@ -185,29 +185,29 @@ function clearClaudeOAuth() {
185
185
  ensureDir(path.dirname(NATIVE_PATHS.claude.settings));
186
186
  claudeSettingsManager.writeSettings(settings);
187
187
  }
188
- function clearClaudeChannelConfig() {
189
- let settings = {};
190
- try {
191
- settings = claudeSettingsManager.settingsExists()
192
- ? claudeSettingsManager.readSettings()
193
- : {};
194
- } catch {
195
- settings = {};
188
+ function clearClaudeChannelConfig(managedProxyUrls = []) {
189
+ if (!claudeSettingsManager.settingsExists()) return;
190
+ const settings = claudeSettingsManager.readSettings();
191
+ if (settings.env && typeof settings.env === 'object' && !Array.isArray(settings.env)) {
192
+ [
193
+ 'ANTHROPIC_BASE_URL',
194
+ 'ANTHROPIC_API_KEY',
195
+ 'ANTHROPIC_AUTH_TOKEN',
196
+ 'CLAUDE_CODE_OAUTH_TOKEN',
197
+ 'ANTHROPIC_MODEL',
198
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
199
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
200
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL'
201
+ ].forEach((key) => {
202
+ delete settings.env[key];
203
+ });
204
+ const managedProxies = new Set((Array.isArray(managedProxyUrls) ? managedProxyUrls : [])
205
+ .map(value => String(value || '').trim()).filter(Boolean));
206
+ ['HTTP_PROXY', 'HTTPS_PROXY'].forEach((key) => {
207
+ if (managedProxies.has(String(settings.env[key] || '').trim())) delete settings.env[key];
208
+ });
209
+ if (Object.keys(settings.env).length === 0) delete settings.env;
196
210
  }
197
-
198
- settings.env = settings.env || {};
199
- [
200
- 'ANTHROPIC_BASE_URL',
201
- 'ANTHROPIC_API_KEY',
202
- 'ANTHROPIC_AUTH_TOKEN',
203
- 'CLAUDE_CODE_OAUTH_TOKEN',
204
- 'ANTHROPIC_MODEL',
205
- 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
206
- 'ANTHROPIC_DEFAULT_SONNET_MODEL',
207
- 'ANTHROPIC_DEFAULT_OPUS_MODEL'
208
- ].forEach((key) => {
209
- delete settings.env[key];
210
- });
211
211
  delete settings.apiKeyHelper;
212
212
  ensureDir(path.dirname(NATIVE_PATHS.claude.settings));
213
213
  claudeSettingsManager.writeSettings(settings);
@@ -444,16 +444,16 @@ function removeCodexChannelEnvVars() {
444
444
  }
445
445
 
446
446
  function clearCodexChannelConfig() {
447
- removeCodexChannelEnvVars();
448
-
449
447
  const configPath = NATIVE_PATHS.codex.config;
448
+ if (!fs.existsSync(configPath)) {
449
+ removeCodexChannelEnvVars();
450
+ return;
451
+ }
450
452
  let config = {};
451
- if (fs.existsSync(configPath)) {
452
- try {
453
- config = toml.parse(fs.readFileSync(configPath, 'utf8'));
454
- } catch {
455
- config = {};
456
- }
453
+ try {
454
+ config = toml.parse(fs.readFileSync(configPath, 'utf8'));
455
+ } catch (error) {
456
+ throw new Error(`Failed to parse existing Codex config.toml: ${error.message}`);
457
457
  }
458
458
 
459
459
  const managedProviderKeys = new Set(['cc-proxy']);
@@ -478,8 +478,8 @@ function clearCodexChannelConfig() {
478
478
  }
479
479
  }
480
480
 
481
- ensureDir(path.dirname(configPath));
482
- fs.writeFileSync(configPath, tomlStringify(config), 'utf8');
481
+ writeTomlFile(configPath, config, { atomic: true });
482
+ removeCodexChannelEnvVars();
483
483
  }
484
484
 
485
485
  function applyCodexOAuth(credential) {