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
@@ -45,11 +45,14 @@ const { EffectiveControlService } = require('./effective-control-service');
45
45
  const { SkillArtifactStore } = require('./skill-artifact-store');
46
46
  const { SkillFormatAdapter } = require('./skill-format-adapters');
47
47
  const { SkillProjectionService } = require('./skill-projection-service');
48
+ const { readTextFileLimited, truncateUtf8 } = require('./bounded-content');
48
49
 
49
50
  const SUPPORTED_REPO_PROVIDERS = ['github', 'gitlab', 'local'];
50
51
  const DEFAULT_GITHUB_HOST = 'https://github.com';
51
52
  const DEFAULT_GITLAB_HOST = 'https://gitlab.com';
52
53
  const CACHE_TTL = 5 * 60 * 1000;
54
+ const MAX_SKILL_DETAIL_BYTES = 128 * 1024;
55
+ const MAX_PREPARED_SKILL_CACHE_ENTRIES = 32;
53
56
 
54
57
  function sanitizeRefreshError(value) {
55
58
  return String(value || '')
@@ -392,7 +395,16 @@ class SkillService {
392
395
  invocation: skill.invocation || null,
393
396
  ...(skill.whenToUse ? { whenToUse: skill.whenToUse } : {}),
394
397
  ...(skill.metadata ? { metadata: skill.metadata } : {}),
395
- ...(includeContent && skill.content !== undefined ? { content: skill.content } : {})
398
+ ...(includeContent && skill.content !== undefined
399
+ ? (() => {
400
+ const bounded = truncateUtf8(skill.content, MAX_SKILL_DETAIL_BYTES);
401
+ return {
402
+ content: bounded.text,
403
+ contentBytes: bounded.bytes,
404
+ contentTruncated: bounded.truncated
405
+ };
406
+ })()
407
+ : {})
396
408
  };
397
409
  }
398
410
 
@@ -534,7 +546,11 @@ class SkillService {
534
546
  return value.map(skill => ({ ...skill }));
535
547
  }
536
548
  const entry = { value, cachedAt: Date.now(), generation };
549
+ this._preparedSkillsCache.delete(cacheKey);
537
550
  this._preparedSkillsCache.set(cacheKey, entry);
551
+ while (this._preparedSkillsCache.size > MAX_PREPARED_SKILL_CACHE_ENTRIES) {
552
+ this._preparedSkillsCache.delete(this._preparedSkillsCache.keys().next().value);
553
+ }
538
554
  if (cacheKey === 'user:') {
539
555
  this.skillsCache = value;
540
556
  this.cacheTime = entry.cachedAt;
@@ -3663,7 +3679,8 @@ ${content}
3663
3679
  if (!fs.existsSync(skillFile) || fs.lstatSync(skillFile).isSymbolicLink()) {
3664
3680
  throw new Error('Skill detail contains an unavailable or symlinked SKILL.md');
3665
3681
  }
3666
- const content = fs.readFileSync(skillFile, 'utf8');
3682
+ const boundedContent = readTextFileLimited(skillFile, MAX_SKILL_DETAIL_BYTES);
3683
+ const content = boundedContent.text;
3667
3684
  const parsed = this.parseSkillMd(content);
3668
3685
  const bodyMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/);
3669
3686
  const body = bodyMatch ? bodyMatch[1].trim() : content;
@@ -3676,6 +3693,8 @@ ${content}
3676
3693
  description: parsed.description || metadata.description || '',
3677
3694
  content: body,
3678
3695
  fullContent: content,
3696
+ contentBytes: boundedContent.bytes,
3697
+ contentTruncated: boundedContent.truncated,
3679
3698
  installed: enabled,
3680
3699
  enabled,
3681
3700
  cached: metadata.cached !== false,
@@ -3881,5 +3900,6 @@ ${content}
3881
3900
  module.exports = {
3882
3901
  SkillService,
3883
3902
  DEFAULT_REPOS: DEFAULT_REPOS_BY_PLATFORM.claude,
3884
- DEFAULT_REPOS_BY_PLATFORM
3903
+ DEFAULT_REPOS_BY_PLATFORM,
3904
+ MAX_SKILL_DETAIL_BYTES
3885
3905
  };
@@ -38,47 +38,6 @@ function sanitizeTimeout(timeout) {
38
38
  return Math.min(Math.max(ms, MIN_TIMEOUT), MAX_TIMEOUT);
39
39
  }
40
40
 
41
- /**
42
- * 规范化批量测速并发度(默认小并发)
43
- */
44
- function sanitizeBatchConcurrency(concurrency, defaultValue = 2) {
45
- const value = Number(concurrency);
46
- if (!Number.isFinite(value) || value <= 0) {
47
- return defaultValue;
48
- }
49
- return Math.min(Math.max(Math.round(value), 1), 5);
50
- }
51
-
52
- /**
53
- * 按并发限制执行异步任务,保持结果顺序与输入一致
54
- */
55
- async function runWithConcurrencyLimit(items, concurrency, taskFn) {
56
- const list = Array.isArray(items) ? items : [];
57
- if (list.length === 0) return [];
58
-
59
- const limit = sanitizeBatchConcurrency(concurrency);
60
- const results = new Array(list.length);
61
- let cursor = 0;
62
-
63
- async function worker() {
64
- while (true) {
65
- const currentIndex = cursor;
66
- cursor += 1;
67
- if (currentIndex >= list.length) {
68
- return;
69
- }
70
- results[currentIndex] = await taskFn(list[currentIndex], currentIndex);
71
- }
72
- }
73
-
74
- const workers = [];
75
- const workerCount = Math.min(limit, list.length);
76
- for (let i = 0; i < workerCount; i += 1) {
77
- workers.push(worker());
78
- }
79
- await Promise.all(workers);
80
- return results;
81
- }
82
41
 
83
42
  function normalizeNonEmptyString(value) {
84
43
  if (typeof value !== 'string') return null;
@@ -768,34 +727,6 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
768
727
  return primaryResult;
769
728
  }
770
729
 
771
- /**
772
- * 批量测试多个渠道
773
- * @param {Array} channels - 渠道列表
774
- * @param {number} timeout - 超时时间
775
- * @param {string} channelType - 渠道类型:'claude' | 'codex' | 'gemini'
776
- * @returns {Promise<Array>} 测试结果列表
777
- */
778
- async function testMultipleChannels(channels, timeout = DEFAULT_TIMEOUT, channelType = 'claude', concurrency = 2) {
779
- const results = await runWithConcurrencyLimit(
780
- channels,
781
- concurrency,
782
- channel => testChannelSpeed(channel, timeout, channelType)
783
- );
784
-
785
- // 按延迟排序(成功的在前,按延迟升序)
786
- results.sort((a, b) => {
787
- if (a.success && !b.success) return -1;
788
- if (!a.success && b.success) return 1;
789
- if (a.success && b.success) {
790
- const aLatency = (a.latency === null || a.latency === undefined) ? Infinity : a.latency;
791
- const bLatency = (b.latency === null || b.latency === undefined) ? Infinity : b.latency;
792
- return aLatency - bLatency;
793
- }
794
- return 0;
795
- });
796
-
797
- return results;
798
- }
799
730
 
800
731
  /**
801
732
  * 获取缓存的测试结果
@@ -834,10 +765,7 @@ function getLatencyLevel(latency) {
834
765
 
835
766
  module.exports = {
836
767
  testChannelSpeed,
837
- testMultipleChannels,
838
768
  getCachedResult,
839
769
  clearCache,
840
770
  getLatencyLevel,
841
- sanitizeBatchConcurrency,
842
- runWithConcurrencyLimit
843
771
  };
@@ -202,6 +202,22 @@ function createChannelDriver({
202
202
  return failed(platform, capability, operation, error);
203
203
  }
204
204
  };
205
+ driver.speedTest = async (request = {}) => {
206
+ const operation = 'speedTest';
207
+ const channelId = request?.params?.channelId || request?.channelId;
208
+ const body = request?.body && typeof request.body === 'object' ? request.body : request;
209
+ if (!channelId) return invalid(platform, capability, operation, new Error('Channel id is required'));
210
+ try {
211
+ const channel = await findChannelById(channelId);
212
+ if (!channel) return invalid(platform, capability, operation, new Error(`Channel not found: ${channelId}`));
213
+ const { testChannelSpeed } = require('../server/services/speed-test');
214
+ const channelType = resolveModelListType(platform, channel, modelListType);
215
+ const data = await testChannelSpeed(channel, body?.timeout, channelType, { authSourceType: platform });
216
+ return ok(platform, capability, operation, data);
217
+ } catch (error) {
218
+ return failed(platform, capability, operation, error);
219
+ }
220
+ };
205
221
  driver.list = (...args) => call('list', listMethod, args, sanitizeChannels);
206
222
  driver.getEnabled = (...args) => call('getEnabled', 'getEnabledChannels', args, channels => sanitizeChannels(channels).channels);
207
223
  driver.create = (input, ...rest) => {
@@ -71,6 +71,7 @@ function argsFor(capability, operation, context) {
71
71
  if (operation === 'update') return [params.channelId, context.body || {}];
72
72
  if (operation === 'remove') return [params.channelId];
73
73
  if (operation === 'models') return [params.channelId, options];
74
+ if (['speedTest', 'getAuth', 'syncLocalAuth', 'getAuthQuota'].includes(operation)) return [context];
74
75
  if (operation === 'applyToSettings') return [params.channelId];
75
76
  return [context.body || options];
76
77
  }
@@ -0,0 +1,474 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const jsonc = require('jsonc-parser');
7
+ const YAML = require('yaml');
8
+ const toml = require('toml');
9
+ const tomlStringify = require('@iarna/toml').stringify;
10
+
11
+ function isPlainObject(value) {
12
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
13
+ }
14
+
15
+ function valuesEqual(left, right) {
16
+ if (left === right) return true;
17
+ if (left instanceof Date || right instanceof Date) {
18
+ if (left instanceof Date && right instanceof Date) return left.getTime() === right.getTime();
19
+ const date = left instanceof Date ? left : right;
20
+ const scalar = left instanceof Date ? right : left;
21
+ return typeof scalar === 'string'
22
+ && Number.isFinite(Date.parse(scalar))
23
+ && Date.parse(scalar) === date.getTime();
24
+ }
25
+ if (Array.isArray(left) || Array.isArray(right)) {
26
+ return Array.isArray(left) && Array.isArray(right)
27
+ && left.length === right.length
28
+ && left.every((value, index) => valuesEqual(value, right[index]));
29
+ }
30
+ if (isPlainObject(left) || isPlainObject(right)) {
31
+ if (!isPlainObject(left) || !isPlainObject(right)) return false;
32
+ const leftKeys = Object.keys(left);
33
+ const rightKeys = Object.keys(right);
34
+ return leftKeys.length === rightKeys.length
35
+ && leftKeys.every(key => Object.prototype.hasOwnProperty.call(right, key)
36
+ && valuesEqual(left[key], right[key]));
37
+ }
38
+ return false;
39
+ }
40
+
41
+ function cloneConfigValue(value) {
42
+ if (value instanceof Date) return new Date(value.getTime());
43
+ if (Array.isArray(value)) return value.map(cloneConfigValue);
44
+ if (isPlainObject(value)) {
45
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, cloneConfigValue(entry)]));
46
+ }
47
+ return value;
48
+ }
49
+
50
+ function ensureParent(filePath) {
51
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
52
+ }
53
+
54
+ function writeContent(filePath, content, options = {}) {
55
+ ensureParent(filePath);
56
+ if (!options.atomic) {
57
+ fs.writeFileSync(filePath, content, 'utf8');
58
+ } else {
59
+ const mode = options.mode !== undefined
60
+ ? options.mode
61
+ : (fs.existsSync(filePath) ? fs.statSync(filePath).mode & 0o777 : 0o600);
62
+ const temporary = `${filePath}.ctx-tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
63
+ try {
64
+ fs.writeFileSync(temporary, content, { encoding: 'utf8', mode });
65
+ if (process.platform !== 'win32') fs.chmodSync(temporary, mode);
66
+ fs.renameSync(temporary, filePath);
67
+ } finally {
68
+ try { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); } catch (_) {}
69
+ }
70
+ }
71
+ if (options.mode !== undefined && process.platform !== 'win32') {
72
+ fs.chmodSync(filePath, options.mode);
73
+ }
74
+ }
75
+
76
+ function parseJsonc(content, filePath = 'config.json') {
77
+ const errors = [];
78
+ const value = jsonc.parse(content, errors, { allowTrailingComma: true, disallowComments: false });
79
+ if (errors.length > 0) {
80
+ const details = errors.map(error => jsonc.printParseErrorCode(error.error)).join(', ');
81
+ throw new Error(`Failed to parse ${path.basename(filePath)}: ${details}`);
82
+ }
83
+ if (!isPlainObject(value)) {
84
+ throw new Error(`${path.basename(filePath)} must contain a JSON object`);
85
+ }
86
+ return value;
87
+ }
88
+
89
+ function reconcileJsonc(content, current, desired, filePath) {
90
+ let output = content;
91
+ const changes = [];
92
+
93
+ function visit(before, after, nodePath) {
94
+ if (valuesEqual(before, after)) return;
95
+ if (isPlainObject(before) && isPlainObject(after)) {
96
+ Object.keys(before).forEach((key) => {
97
+ if (!Object.prototype.hasOwnProperty.call(after, key)) {
98
+ changes.push({ path: [...nodePath, key], value: undefined });
99
+ }
100
+ });
101
+ Object.keys(after).forEach((key) => {
102
+ if (!Object.prototype.hasOwnProperty.call(before, key)) {
103
+ changes.push({ path: [...nodePath, key], value: after[key] });
104
+ } else {
105
+ visit(before[key], after[key], [...nodePath, key]);
106
+ }
107
+ });
108
+ return;
109
+ }
110
+ changes.push({ path: nodePath, value: after });
111
+ }
112
+
113
+ visit(current, desired, []);
114
+ for (const change of changes) {
115
+ const edits = jsonc.modify(output, change.path, change.value, {
116
+ formattingOptions: {
117
+ insertSpaces: true,
118
+ tabSize: 2,
119
+ eol: output.includes('\r\n') ? '\r\n' : '\n'
120
+ }
121
+ });
122
+ output = jsonc.applyEdits(output, edits);
123
+ }
124
+ return output;
125
+ }
126
+
127
+ function updateJsoncFile(filePath, mutate, options = {}) {
128
+ const exists = fs.existsSync(filePath);
129
+ const original = exists ? fs.readFileSync(filePath, 'utf8') : '{}\n';
130
+ const current = original.trim() ? parseJsonc(original, filePath) : {};
131
+ const desired = cloneConfigValue(current);
132
+ mutate(desired);
133
+ const output = reconcileJsonc(original, current, desired, filePath);
134
+ if (output !== original) {
135
+ writeContent(filePath, output, options);
136
+ }
137
+ return desired;
138
+ }
139
+
140
+ function readJsoncFile(filePath, fallback = {}) {
141
+ if (!fs.existsSync(filePath)) return JSON.parse(JSON.stringify(fallback));
142
+ const original = fs.readFileSync(filePath, 'utf8');
143
+ if (!original.trim()) return JSON.parse(JSON.stringify(fallback));
144
+ return parseJsonc(original, filePath);
145
+ }
146
+
147
+ function writeJsoncFile(filePath, value, options = {}) {
148
+ return updateJsoncFile(filePath, (current) => {
149
+ Object.keys(current).forEach(key => delete current[key]);
150
+ Object.assign(current, cloneConfigValue(value || {}));
151
+ }, options);
152
+ }
153
+
154
+ function reconcileYamlDocument(document, current, desired, nodePath = []) {
155
+ if (valuesEqual(current, desired)) return;
156
+ if (isPlainObject(current) && isPlainObject(desired)) {
157
+ Object.keys(current).forEach((key) => {
158
+ if (!Object.prototype.hasOwnProperty.call(desired, key)) {
159
+ document.deleteIn([...nodePath, key]);
160
+ }
161
+ });
162
+ Object.keys(desired).forEach((key) => {
163
+ if (!Object.prototype.hasOwnProperty.call(current, key)) {
164
+ document.setIn([...nodePath, key], desired[key]);
165
+ } else {
166
+ reconcileYamlDocument(document, current[key], desired[key], [...nodePath, key]);
167
+ }
168
+ });
169
+ return;
170
+ }
171
+ document.setIn(nodePath, desired);
172
+ }
173
+
174
+ function updateYamlFile(filePath, mutate, options = {}) {
175
+ const exists = fs.existsSync(filePath);
176
+ const original = exists ? fs.readFileSync(filePath, 'utf8') : '{}\n';
177
+ const document = YAML.parseDocument(original, {
178
+ keepSourceTokens: true,
179
+ prettyErrors: true,
180
+ customTags: options.customTags
181
+ });
182
+ if (document.errors.length > 0) {
183
+ throw new Error(`Failed to parse ${path.basename(filePath)}: ${document.errors[0].message}`);
184
+ }
185
+ let current = document.toJS({ mapAsMap: false });
186
+ if (current === undefined || current === null) current = {};
187
+ else if (!isPlainObject(current)) {
188
+ throw new Error(`${path.basename(filePath)} must contain a YAML mapping`);
189
+ }
190
+ const desired = cloneConfigValue(current);
191
+ mutate(desired);
192
+ if (valuesEqual(current, desired)) return desired;
193
+ reconcileYamlDocument(document, current, desired);
194
+ const output = document.toString({ lineWidth: 120 });
195
+ writeContent(filePath, output, options);
196
+ return desired;
197
+ }
198
+
199
+ function writeYamlFile(filePath, value, options = {}) {
200
+ return updateYamlFile(filePath, (current) => {
201
+ Object.keys(current).forEach(key => delete current[key]);
202
+ Object.assign(current, cloneConfigValue(value || {}));
203
+ }, options);
204
+ }
205
+
206
+ function findTomlCommentStart(line) {
207
+ let quote = '';
208
+ let escaped = false;
209
+ for (let index = 0; index < line.length; index += 1) {
210
+ const char = line[index];
211
+ if (escaped) { escaped = false; continue; }
212
+ if (char === '\\' && quote === '"') { escaped = true; continue; }
213
+ if ((char === '"' || char === "'") && (!quote || quote === char)) {
214
+ quote = quote ? '' : char;
215
+ continue;
216
+ }
217
+ if (char === '#' && !quote) return index;
218
+ }
219
+ return -1;
220
+ }
221
+
222
+ function stripTomlComment(line) {
223
+ const commentIndex = findTomlCommentStart(line);
224
+ return commentIndex < 0 ? line : line.slice(0, commentIndex);
225
+ }
226
+
227
+ function tomlHeaderProviderKey(line) {
228
+ const header = stripTomlComment(line).trim();
229
+ const match = header.match(/^\[\s*model_providers\s*\.\s*(?:"((?:\\.|[^"\\])*)"|'([^']*)'|([A-Za-z0-9_-]+))\s*\]$/);
230
+ if (!match) return null;
231
+ if (match[1] !== undefined) {
232
+ try { return JSON.parse(`"${match[1]}"`); } catch (_) { return null; }
233
+ }
234
+ return match[2] !== undefined ? match[2] : match[3];
235
+ }
236
+
237
+ function isNestedTomlProviderSection(line, providerKey) {
238
+ const escaped = String(providerKey).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
239
+ return new RegExp(`^\\s*\\[\\s*model_providers\\s*\\.\\s*(?:"${escaped}"|'${escaped}'|${escaped})\\s*\\.`)
240
+ .test(stripTomlComment(line).trimEnd());
241
+ }
242
+
243
+ function preserveTomlSectionPreamble(lines, sectionStart, nextSectionStart) {
244
+ let boundary = nextSectionStart;
245
+ while (boundary > sectionStart + 1) {
246
+ const line = lines[boundary - 1].trim();
247
+ if (line === '' || line.startsWith('#')) boundary -= 1;
248
+ else break;
249
+ }
250
+ return boundary;
251
+ }
252
+
253
+ function patchTomlProvider(content, providerKey, providerValue, eol) {
254
+ const lines = content.split(/(?<=\n)/);
255
+ const starts = [];
256
+ lines.forEach((line, index) => {
257
+ const parsedKey = tomlHeaderProviderKey(line.replace(/\r?\n$/, ''));
258
+ if (parsedKey === providerKey) starts.push(index);
259
+ });
260
+ const start = starts[0];
261
+
262
+ if (providerValue === undefined) {
263
+ if (start === undefined) return content;
264
+ let end = start + 1;
265
+ while (end < lines.length) {
266
+ if (!/^\s*\[(?!\[).+\]\s*(?:#.*)?(?:\r?\n)?$/.test(lines[end])) {
267
+ end += 1;
268
+ continue;
269
+ }
270
+ if (isNestedTomlProviderSection(lines[end], providerKey)) {
271
+ end += 1;
272
+ continue;
273
+ }
274
+ break;
275
+ }
276
+ end = preserveTomlSectionPreamble(lines, start, end);
277
+ lines.splice(start, end - start);
278
+ return lines.join('');
279
+ }
280
+
281
+ const generated = tomlStringify({ model_providers: { [providerKey]: providerValue } });
282
+ const generatedLines = generated.split(/(?<=\n)/);
283
+ const sectionStart = generatedLines.findIndex(line => tomlHeaderProviderKey(line.replace(/\r?\n$/, '')) === providerKey);
284
+ if (sectionStart < 0) throw new Error(`Failed to serialize Codex provider ${providerKey}`);
285
+ const section = generatedLines.slice(sectionStart).join('').trimEnd();
286
+
287
+ if (start !== undefined) {
288
+ let end = start + 1;
289
+ while (end < lines.length) {
290
+ if (!/^\s*\[(?!\[).+\]\s*(?:#.*)?(?:\r?\n)?$/.test(lines[end])) {
291
+ end += 1;
292
+ continue;
293
+ }
294
+ if (isNestedTomlProviderSection(lines[end], providerKey)) {
295
+ end += 1;
296
+ continue;
297
+ }
298
+ break;
299
+ }
300
+ const replaceEnd = preserveTomlSectionPreamble(lines, start, end);
301
+ const preservedComments = lines.slice(start, replaceEnd)
302
+ .map((line) => {
303
+ const withoutNewline = line.replace(/\r?\n$/, '');
304
+ const commentIndex = findTomlCommentStart(withoutNewline);
305
+ return commentIndex < 0 ? '' : withoutNewline.slice(commentIndex).trim();
306
+ })
307
+ .filter(Boolean);
308
+ const commentText = preservedComments.length > 0
309
+ ? `${eol}${preservedComments.join(eol)}`
310
+ : '';
311
+ lines.splice(start, replaceEnd - start, `${section}${commentText}${eol}`);
312
+ return lines.join('');
313
+ }
314
+
315
+ let output = content;
316
+ if (output && !output.endsWith('\n')) output += eol;
317
+ if (output && !output.endsWith(`${eol}${eol}`)) output += eol;
318
+ return `${output}${section}${eol}`;
319
+ }
320
+
321
+ function writeTomlFile(filePath, desired, options = {}) {
322
+ const exists = fs.existsSync(filePath);
323
+ const original = exists ? fs.readFileSync(filePath, 'utf8') : '';
324
+ let current = {};
325
+ if (original.trim()) {
326
+ try {
327
+ current = toml.parse(original);
328
+ } catch (error) {
329
+ throw new Error(`Failed to parse ${path.basename(filePath)}: ${error.message}`);
330
+ }
331
+ }
332
+ const before = current && typeof current === 'object' ? current : {};
333
+ const after = desired && typeof desired === 'object' ? desired : {};
334
+ const allowed = new Set(['model_provider', 'model_providers']);
335
+ const otherKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
336
+ otherKeys.forEach(key => {
337
+ if (!allowed.has(key) && !valuesEqual(before[key], after[key])) {
338
+ throw new Error(`Refusing to rewrite unrelated TOML field "${key}" in ${path.basename(filePath)}`);
339
+ }
340
+ });
341
+
342
+ const eol = original.includes('\r\n') ? '\r\n' : '\n';
343
+ let output = original;
344
+ const hasCurrentProvider = Object.prototype.hasOwnProperty.call(before, 'model_provider');
345
+ const hasDesiredProvider = Object.prototype.hasOwnProperty.call(after, 'model_provider');
346
+ const providerChanged = hasCurrentProvider !== hasDesiredProvider
347
+ || (hasCurrentProvider && before.model_provider !== after.model_provider);
348
+ if (providerChanged) {
349
+ const rootLines = output.split(/(?<=\n)/);
350
+ const headerIndex = rootLines.findIndex(line => /^\s*\[(?!\[).+\]/.test(line));
351
+ const rootEnd = headerIndex < 0 ? rootLines.length : headerIndex;
352
+ const settingIndex = rootLines.slice(0, rootEnd).findIndex(line => /^\s*model_provider\s*=/.test(line));
353
+ if (hasDesiredProvider) {
354
+ const serialized = tomlStringify({ model_provider: after.model_provider }).trimEnd();
355
+ if (settingIndex >= 0) {
356
+ const line = rootLines[settingIndex];
357
+ const newline = line.match(/\r?\n$/)?.[0] || '';
358
+ const assignment = line.replace(/\r?\n$/, '');
359
+ let commentIndex = -1;
360
+ let quoted = false;
361
+ let escaped = false;
362
+ for (let index = 0; index < assignment.length; index += 1) {
363
+ const char = assignment[index];
364
+ if (escaped) { escaped = false; continue; }
365
+ if (char === '\\' && quoted) { escaped = true; continue; }
366
+ if (char === '"') { quoted = !quoted; continue; }
367
+ if (char === '#' && !quoted) { commentIndex = index; break; }
368
+ }
369
+ const suffix = commentIndex >= 0 ? ` ${assignment.slice(commentIndex)}` : '';
370
+ const prefix = assignment.slice(0, assignment.indexOf('=') + 1);
371
+ rootLines[settingIndex] = `${prefix} ${serialized.slice(serialized.indexOf('=') + 1).trim()}${suffix}${newline}`;
372
+ } else {
373
+ const insertion = `${serialized}${eol}`;
374
+ rootLines.splice(rootEnd, 0, `${insertion}${rootEnd < rootLines.length ? eol : ''}`);
375
+ }
376
+ } else if (settingIndex >= 0) {
377
+ const line = rootLines[settingIndex];
378
+ const commentIndex = line.indexOf('#');
379
+ if (commentIndex >= 0) rootLines[settingIndex] = `${line.slice(commentIndex).trimStart()}`;
380
+ else rootLines.splice(settingIndex, 1);
381
+ }
382
+ output = rootLines.join('');
383
+ }
384
+
385
+ const currentProviders = isPlainObject(before.model_providers) ? before.model_providers : {};
386
+ const desiredProviders = isPlainObject(after.model_providers) ? after.model_providers : {};
387
+ const providerKeys = new Set([...Object.keys(currentProviders), ...Object.keys(desiredProviders)]);
388
+ for (const key of providerKeys) {
389
+ if (valuesEqual(currentProviders[key], desiredProviders[key])) continue;
390
+ output = patchTomlProvider(output, key, Object.prototype.hasOwnProperty.call(desiredProviders, key)
391
+ ? desiredProviders[key]
392
+ : undefined, eol);
393
+ }
394
+ if (output !== original) writeContent(filePath, output, options);
395
+ return after;
396
+ }
397
+
398
+ function encodeEnvValue(value) {
399
+ const text = String(value ?? '');
400
+ if (!/[\s#'"\\]/.test(text) && !text.startsWith('=') && !text.endsWith('\r')) return text;
401
+ return JSON.stringify(text);
402
+ }
403
+
404
+ function findEnvComment(value) {
405
+ let quote = '';
406
+ let escaped = false;
407
+ for (let index = 0; index < value.length; index += 1) {
408
+ const char = value[index];
409
+ if (escaped) { escaped = false; continue; }
410
+ if (char === '\\' && quote === '"') { escaped = true; continue; }
411
+ if ((char === '"' || char === "'") && (!quote || quote === char)) {
412
+ quote = quote ? '' : char;
413
+ continue;
414
+ }
415
+ if (char === '#' && !quote && index > 0 && /\s/.test(value[index - 1])) {
416
+ let start = index;
417
+ while (start > 0 && /\s/.test(value[start - 1])) start -= 1;
418
+ return value.slice(start);
419
+ }
420
+ }
421
+ return '';
422
+ }
423
+
424
+ function updateEnvFile(filePath, updates, options = {}) {
425
+ const exists = fs.existsSync(filePath);
426
+ const original = exists ? fs.readFileSync(filePath, 'utf8') : '';
427
+ const eol = original.includes('\r\n') ? '\r\n' : '\n';
428
+ const lines = original.split(/\r?\n/);
429
+ const keys = new Set(Object.keys(updates || {}));
430
+ const seen = new Set();
431
+ const output = [];
432
+
433
+ for (const line of lines) {
434
+ const match = line.match(/^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)\s*=([^\r\n]*)$/);
435
+ if (!match || !keys.has(match[2])) {
436
+ output.push(line);
437
+ continue;
438
+ }
439
+ const [, prefix, key] = match;
440
+ const value = updates[key];
441
+ const rawValue = match[3];
442
+ const comment = findEnvComment(rawValue);
443
+ if (value === undefined || value === null) {
444
+ if (comment) output.push(comment.trimStart());
445
+ continue;
446
+ }
447
+ output.push(`${prefix}${key}=${encodeEnvValue(value)}${comment ? ` ${comment.trimStart()}` : ''}`);
448
+ seen.add(key);
449
+ }
450
+
451
+ const missing = [...keys].filter(key => updates[key] !== undefined && updates[key] !== null && !seen.has(key));
452
+ if (missing.length > 0) {
453
+ while (output.length > 0 && output[output.length - 1] === '') output.pop();
454
+ output.push(...missing.map(key => `${key}=${encodeEnvValue(updates[key])}`));
455
+ }
456
+
457
+ let content = output.join(eol);
458
+ if (content && !content.endsWith(eol) && (original.endsWith('\n') || missing.length > 0)) content += eol;
459
+ if (content !== original) {
460
+ writeContent(filePath, content, options);
461
+ }
462
+ return content;
463
+ }
464
+
465
+ module.exports = {
466
+ parseJsonc,
467
+ readJsoncFile,
468
+ writeJsoncFile,
469
+ updateJsoncFile,
470
+ updateYamlFile,
471
+ writeYamlFile,
472
+ writeTomlFile,
473
+ updateEnvFile
474
+ };