coding-tool-x 3.3.6 → 3.3.8

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 (56) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/web/assets/{Analytics-TtaduRqL.js → Analytics-DLpoDZ2M.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-BP2lLBMN.js → ConfigTemplates-D_hRb55W.js} +1 -1
  4. package/dist/web/assets/Home-BMoFdAwy.css +1 -0
  5. package/dist/web/assets/Home-DNwp-0J-.js +1 -0
  6. package/dist/web/assets/{PluginManager-HmISlyMK.js → PluginManager-JXsyym1s.js} +1 -1
  7. package/dist/web/assets/{ProjectList-DoN8Hjbu.js → ProjectList-DZWSeb-q.js} +1 -1
  8. package/dist/web/assets/{SessionList-Da8BYzNi.js → SessionList-Cs624DR3.js} +1 -1
  9. package/dist/web/assets/{SkillManager-DqLAXh9o.js → SkillManager-bEliz7qz.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-B_TxOgPW.js → WorkspaceManager-J3RecFGn.js} +1 -1
  11. package/dist/web/assets/{icons-B29onFfZ.js → icons-Cuc23WS7.js} +1 -1
  12. package/dist/web/assets/index-BXeSvAwU.js +2 -0
  13. package/dist/web/assets/index-DWAC3Tdv.css +1 -0
  14. package/dist/web/index.html +3 -3
  15. package/package.json +3 -2
  16. package/src/commands/daemon.js +44 -6
  17. package/src/commands/toggle-proxy.js +100 -5
  18. package/src/config/default.js +1 -1
  19. package/src/config/model-metadata.js +2 -2
  20. package/src/config/model-metadata.json +7 -2
  21. package/src/config/paths.js +102 -19
  22. package/src/server/api/channels.js +9 -0
  23. package/src/server/api/codex-channels.js +9 -0
  24. package/src/server/api/codex-proxy.js +22 -11
  25. package/src/server/api/gemini-proxy.js +22 -11
  26. package/src/server/api/mcp.js +26 -4
  27. package/src/server/api/oauth-credentials.js +163 -0
  28. package/src/server/api/opencode-proxy.js +22 -10
  29. package/src/server/api/plugins.js +3 -1
  30. package/src/server/api/proxy.js +39 -44
  31. package/src/server/api/skills.js +91 -13
  32. package/src/server/codex-proxy-server.js +1 -11
  33. package/src/server/index.js +26 -2
  34. package/src/server/services/channels.js +18 -22
  35. package/src/server/services/codex-channels.js +124 -175
  36. package/src/server/services/codex-config.js +2 -5
  37. package/src/server/services/codex-settings-manager.js +12 -348
  38. package/src/server/services/config-export-service.js +572 -117
  39. package/src/server/services/gemini-channels.js +11 -9
  40. package/src/server/services/mcp-client.js +70 -13
  41. package/src/server/services/mcp-service.js +74 -29
  42. package/src/server/services/model-detector.js +1 -0
  43. package/src/server/services/native-keychain.js +243 -0
  44. package/src/server/services/native-oauth-adapters.js +890 -0
  45. package/src/server/services/oauth-credentials-service.js +786 -0
  46. package/src/server/services/oauth-utils.js +49 -0
  47. package/src/server/services/opencode-channels.js +13 -9
  48. package/src/server/services/opencode-settings-manager.js +169 -16
  49. package/src/server/services/plugins-service.js +22 -1
  50. package/src/server/services/settings-manager.js +13 -0
  51. package/src/server/services/skill-service.js +712 -332
  52. package/src/utils/port-helper.js +87 -2
  53. package/dist/web/assets/Home-BsSioaaB.css +0 -1
  54. package/dist/web/assets/Home-CbbyopS-.js +0 -1
  55. package/dist/web/assets/index-By3mDEvx.js +0 -2
  56. package/dist/web/assets/index-CsWInMQV.css +0 -1
@@ -6,7 +6,8 @@ const toml = require('toml');
6
6
  const tomlStringify = require('@iarna/toml').stringify;
7
7
  const { resolvePreferredHomeDir } = require('../../utils/home-dir');
8
8
  const { getCodexDir } = require('./codex-config');
9
- const { injectEnvToShell, removeEnvFromShell, isProxyConfig } = require('./codex-settings-manager');
9
+ const { isProxyConfig } = require('./codex-settings-manager');
10
+ const { clearNativeOAuth } = require('./native-oauth-adapters');
10
11
 
11
12
  const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
12
13
 
@@ -129,13 +130,7 @@ function initializeFromConfig() {
129
130
  updatedAt: Date.now()
130
131
  });
131
132
 
132
- // 自动注入环境变量(从 Codex 迁移过来时使用)
133
- if (apiKey && envKey) {
134
- const injectResult = injectEnvToShell(envKey, apiKey);
135
- if (injectResult.success) {
136
- console.log(`[Codex Channels] Environment variable ${envKey} injected during initialization`);
137
- }
138
- }
133
+ // auth.json 已写入 API Key,Codex 启动时优先读取 auth.json,无需注入 shell
139
134
  }
140
135
  }
141
136
 
@@ -158,6 +153,86 @@ function saveChannels(data) {
158
153
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
159
154
  }
160
155
 
156
+ function getDefaultCodexConfig() {
157
+ return {
158
+ model: 'gpt-4',
159
+ model_reasoning_effort: 'high',
160
+ model_reasoning_summary_format: 'experimental',
161
+ network_access: 'enabled',
162
+ disable_response_storage: false,
163
+ show_raw_agent_reasoning: true
164
+ };
165
+ }
166
+
167
+ function cloneConfigValue(value) {
168
+ return JSON.parse(JSON.stringify(value));
169
+ }
170
+
171
+ function readCodexConfigOrThrow(configPath, fallbackConfig = {}) {
172
+ if (!fs.existsSync(configPath)) {
173
+ return cloneConfigValue(fallbackConfig);
174
+ }
175
+
176
+ const content = fs.readFileSync(configPath, 'utf8');
177
+
178
+ try {
179
+ return toml.parse(content);
180
+ } catch (err) {
181
+ throw new Error(`Failed to parse existing config.toml: ${err.message}`);
182
+ }
183
+ }
184
+
185
+ function writeTextAtomic(filePath, content) {
186
+ const dirPath = path.dirname(filePath);
187
+ if (!fs.existsSync(dirPath)) {
188
+ fs.mkdirSync(dirPath, { recursive: true });
189
+ }
190
+
191
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
192
+
193
+ try {
194
+ fs.writeFileSync(tempPath, content, 'utf8');
195
+ fs.renameSync(tempPath, filePath);
196
+ } finally {
197
+ if (fs.existsSync(tempPath)) {
198
+ try {
199
+ fs.unlinkSync(tempPath);
200
+ } catch {
201
+ // ignore temp cleanup errors
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ function writeAnnotatedCodexConfig(configPath, config, headerLines = []) {
208
+ const tomlContent = tomlStringify(config);
209
+ const prefix = headerLines.length > 0 ? `${headerLines.join('\n')}\n\n` : '';
210
+ writeTextAtomic(configPath, `${prefix}${tomlContent}`);
211
+ }
212
+
213
+ function getManagedProviderKeys(channels = []) {
214
+ const keys = new Set(['cc-proxy']);
215
+ for (const channel of channels) {
216
+ if (channel?.providerKey) {
217
+ keys.add(channel.providerKey);
218
+ }
219
+ }
220
+ return keys;
221
+ }
222
+
223
+ function pruneManagedProviders(existingProviders = {}, currentProviderKey, channels = []) {
224
+ const managedProviderKeys = getManagedProviderKeys(channels);
225
+ const preservedProviders = {};
226
+
227
+ for (const [providerKey, providerConfig] of Object.entries(existingProviders)) {
228
+ if (!managedProviderKeys.has(providerKey) || providerKey === currentProviderKey) {
229
+ preservedProviders[providerKey] = providerConfig;
230
+ }
231
+ }
232
+
233
+ return preservedProviders;
234
+ }
235
+
161
236
  // 获取所有渠道
162
237
  function getChannels() {
163
238
  const data = loadChannels();
@@ -202,15 +277,7 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
202
277
  data.channels.push(newChannel);
203
278
  saveChannels(data);
204
279
 
205
- // 注入该渠道的环境变量(用于直接使用 codex 命令)
206
- if (newChannel.enabled !== false && newChannel.apiKey && envKey) {
207
- const injectResult = injectEnvToShell(envKey, newChannel.apiKey);
208
- if (injectResult.success) {
209
- console.log(`[Codex Channels] Environment variable ${envKey} injected for new channel`);
210
- } else {
211
- console.warn(`[Codex Channels] Failed to inject ${envKey}: ${injectResult.error}`);
212
- }
213
- }
280
+ // auth.json 已写入 API Key(通过 writeCodexConfigForMultiChannel),Codex 优先读取 auth.json
214
281
 
215
282
  // 注意:不再自动写入 config.toml,只在开启代理控制时才同步
216
283
  // writeCodexConfigForMultiChannel(data.channels);
@@ -266,14 +333,6 @@ function updateChannel(channelId, updates) {
266
333
  console.log(`[Codex Single-channel mode] Enabled "${newChannel.name}", disabled all others`);
267
334
  }
268
335
 
269
- // Prevent disabling last enabled channel when proxy is OFF
270
- if (!isProxyRunning && !newChannel.enabled && oldChannel.enabled) {
271
- const enabledCount = data.channels.filter(ch => ch.enabled).length;
272
- if (enabledCount === 0) {
273
- throw new Error('无法禁用最后一个启用的渠道。请先启用其他渠道或启动动态切换。');
274
- }
275
- }
276
-
277
336
  saveChannels(data);
278
337
 
279
338
  // Sync config.toml only when proxy is OFF.
@@ -295,20 +354,7 @@ function updateChannel(channelId, updates) {
295
354
  newChannel.enabled === false
296
355
  );
297
356
 
298
- // 禁用或 key 变化时都要清理旧环境变量,避免残留
299
- if (shouldRemoveOldEnv) {
300
- const removeResult = removeEnvFromShell(oldEnvKey);
301
- if (removeResult.success) {
302
- console.log(`[Codex Channels] Old environment variable ${oldEnvKey} removed`);
303
- }
304
- }
305
-
306
- if (newChannel.enabled !== false && newApiKey && newEnvKey) {
307
- const injectResult = injectEnvToShell(newEnvKey, newApiKey);
308
- if (injectResult.success) {
309
- console.log(`[Codex Channels] Environment variable ${newEnvKey} updated`);
310
- }
311
- }
357
+ // auth.json applyChannelToSettings/writeCodexConfigForMultiChannel 维护,Codex 优先读取 auth.json
312
358
 
313
359
  // 注意:不再自动写入 config.toml,只在开启代理控制时才同步
314
360
  // writeCodexConfigForMultiChannel(data.channels);
@@ -329,15 +375,7 @@ async function deleteChannel(channelId) {
329
375
  data.channels.splice(index, 1);
330
376
  saveChannels(data);
331
377
 
332
- // shell 配置文件移除该渠道的环境变量
333
- if (deletedChannel.envKey) {
334
- const removeResult = removeEnvFromShell(deletedChannel.envKey);
335
- if (removeResult.success) {
336
- console.log(`[Codex Channels] Environment variable ${deletedChannel.envKey} removed`);
337
- } else {
338
- console.warn(`[Codex Channels] Failed to remove ${deletedChannel.envKey}: ${removeResult.error}`);
339
- }
340
- }
378
+ // auth.json 中的 key 由 writeCodexConfigForMultiChannel 管理,删除渠道后下次写入时自动清理
341
379
 
342
380
  // 注意:不再自动写入 config.toml,只在开启代理控制时才同步
343
381
  // writeCodexConfigForMultiChannel(data.channels);
@@ -364,38 +402,18 @@ function writeCodexConfigForMultiChannel(allChannels) {
364
402
  const authPath = path.join(codexDir, 'auth.json');
365
403
 
366
404
  // 读取现有配置,保留所有现有字段(特别是 mcp_servers, projects 等)
405
+ const defaultConfig = getDefaultCodexConfig();
406
+ const parsedConfig = readCodexConfigOrThrow(configPath, defaultConfig);
367
407
  let config = {
368
- model: 'gpt-4',
369
- model_reasoning_effort: 'high',
370
- model_reasoning_summary_format: 'experimental',
371
- network_access: 'enabled',
372
- disable_response_storage: false,
373
- show_raw_agent_reasoning: true
408
+ ...parsedConfig,
409
+ model: parsedConfig.model || defaultConfig.model,
410
+ model_reasoning_effort: parsedConfig.model_reasoning_effort || defaultConfig.model_reasoning_effort,
411
+ model_reasoning_summary_format: parsedConfig.model_reasoning_summary_format || defaultConfig.model_reasoning_summary_format,
412
+ network_access: parsedConfig.network_access || defaultConfig.network_access,
413
+ disable_response_storage: parsedConfig.disable_response_storage !== undefined ? parsedConfig.disable_response_storage : defaultConfig.disable_response_storage,
414
+ show_raw_agent_reasoning: parsedConfig.show_raw_agent_reasoning !== undefined ? parsedConfig.show_raw_agent_reasoning : defaultConfig.show_raw_agent_reasoning
374
415
  };
375
416
 
376
- if (fs.existsSync(configPath)) {
377
- try {
378
- const content = fs.readFileSync(configPath, 'utf8');
379
- const parsedConfig = toml.parse(content);
380
-
381
- // 深度合并,保留原有的所有配置
382
- config = {
383
- ...parsedConfig,
384
- // 只覆盖这些字段
385
- model: parsedConfig.model || config.model,
386
- model_reasoning_effort: parsedConfig.model_reasoning_effort || config.model_reasoning_effort,
387
- model_reasoning_summary_format: parsedConfig.model_reasoning_summary_format || config.model_reasoning_summary_format,
388
- network_access: parsedConfig.network_access || config.network_access,
389
- disable_response_storage: parsedConfig.disable_response_storage !== undefined ? parsedConfig.disable_response_storage : config.disable_response_storage,
390
- show_raw_agent_reasoning: parsedConfig.show_raw_agent_reasoning !== undefined ? parsedConfig.show_raw_agent_reasoning : config.show_raw_agent_reasoning,
391
- // mcp_servers 和 projects 会从 parsedConfig 自动继承
392
- // model_provider 会根据动态切换情况决定是否更新
393
- };
394
- } catch (err) {
395
- // ignore read error, use defaults
396
- }
397
- }
398
-
399
417
  // 判断是否已启用动态切换
400
418
  const isProxyMode = config.model_provider === 'cc-proxy';
401
419
  const existingProviders = (config && typeof config.model_providers === 'object') ? config.model_providers : {};
@@ -441,23 +459,11 @@ function writeCodexConfigForMultiChannel(allChannels) {
441
459
  }
442
460
  }
443
461
 
444
- // 使用 TOML 序列化写入配置(保留注释和格式)
445
- try {
446
- const tomlContent = tomlStringify(config);
447
- // 在开头添加标记注释
448
- const annotatedContent = `# Codex Configuration
449
- # Managed by Coding-Tool
450
- # WARNING: MCP servers and projects are preserved automatically
451
-
452
- ${tomlContent}`;
453
-
454
- fs.writeFileSync(configPath, annotatedContent, 'utf8');
455
- } catch (err) {
456
- console.error('[Codex Channels] Failed to write config with TOML stringify:', err);
457
- // 降级处理:如果 tomlStringify 失败,使用手工拼接(但这样会丢失注释)
458
- const fallbackContent = JSON.stringify(config, null, 2);
459
- fs.writeFileSync(configPath, fallbackContent, 'utf8');
460
- }
462
+ writeAnnotatedCodexConfig(configPath, config, [
463
+ '# Codex Configuration',
464
+ '# Managed by Coding-Tool',
465
+ '# WARNING: MCP servers and projects are preserved automatically'
466
+ ]);
461
467
 
462
468
  // 更新 auth.json
463
469
  let auth = {};
@@ -524,42 +530,8 @@ function saveChannelOrder(order) {
524
530
  * 这个函数会在服务启动时自动调用
525
531
  */
526
532
  function syncAllChannelEnvVars() {
527
- try {
528
- const data = loadChannels();
529
- const channels = data.channels || [];
530
-
531
- if (channels.length === 0) {
532
- return { success: true, synced: 0 };
533
- }
534
-
535
- let syncedCount = 0;
536
- const results = [];
537
-
538
- for (const channel of channels) {
539
- if (!channel.envKey) continue;
540
-
541
- const shouldInject = channel.enabled !== false && !!channel.apiKey;
542
- if (shouldInject) {
543
- const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
544
- if (injectResult.success) {
545
- syncedCount++;
546
- results.push({ envKey: channel.envKey, success: true });
547
- } else {
548
- results.push({ envKey: channel.envKey, success: false, error: injectResult.error });
549
- }
550
- continue;
551
- }
552
-
553
- // 清理已停用或缺失 key 的渠道环境变量,避免残留
554
- removeEnvFromShell(channel.envKey);
555
- }
556
-
557
- console.log(`[Codex Channels] Synced ${syncedCount} environment variables`);
558
- return { success: true, synced: syncedCount, results };
559
- } catch (err) {
560
- console.error('[Codex Channels] Failed to sync env vars:', err);
561
- return { success: false, error: err.message };
562
- }
533
+ // Codex 优先从 auth.json 读取 API Key,无需注入 shell 配置文件
534
+ return { success: true, synced: 0 };
563
535
  }
564
536
 
565
537
  /**
@@ -584,6 +556,7 @@ function applyChannelToSettings(channelId, options = {}) {
584
556
  ch.enabled = ch.id === channelId;
585
557
  });
586
558
  saveChannels(data);
559
+ clearNativeOAuth('codex');
587
560
 
588
561
  const codexDir = getCodexDir();
589
562
 
@@ -595,33 +568,18 @@ function applyChannelToSettings(channelId, options = {}) {
595
568
  const authPath = path.join(codexDir, 'auth.json');
596
569
 
597
570
  // 读取现有配置,保留 mcp_servers, projects 等
598
- let config = {
599
- model: 'gpt-4',
600
- model_reasoning_effort: 'high',
601
- model_reasoning_summary_format: 'experimental',
602
- network_access: 'enabled',
603
- disable_response_storage: false,
604
- show_raw_agent_reasoning: true
605
- };
606
-
607
- if (fs.existsSync(configPath)) {
608
- try {
609
- const content = fs.readFileSync(configPath, 'utf8');
610
- const parsedConfig = toml.parse(content);
611
- // 深度合并,保留原有的所有配置
612
- config = { ...parsedConfig };
613
- } catch (err) {
614
- console.warn('[Codex Channels] Failed to read existing config, using defaults');
615
- }
616
- }
571
+ let config = readCodexConfigOrThrow(configPath, getDefaultCodexConfig());
617
572
 
618
573
  // 设置当前渠道为 model_provider
619
574
  config.model_provider = channel.providerKey;
620
575
 
621
576
  // 可选:清理 provider,关闭动态切换后只保留当前渠道配置
622
577
  if (options.pruneProviders === true) {
623
- config.model_providers = {};
624
- } else if (!config.model_providers) {
578
+ const existingProviders = (config.model_providers && typeof config.model_providers === 'object')
579
+ ? config.model_providers
580
+ : {};
581
+ config.model_providers = pruneManagedProviders(existingProviders, channel.providerKey, data.channels);
582
+ } else if (!config.model_providers || typeof config.model_providers !== 'object') {
625
583
  // 默认兼容历史行为:保留已有 provider
626
584
  config.model_providers = {};
627
585
  }
@@ -640,21 +598,12 @@ function applyChannelToSettings(channelId, options = {}) {
640
598
  config.model_providers[channel.providerKey].query_params = channel.queryParams;
641
599
  }
642
600
 
643
- // 使用 TOML 序列化写入配置
644
- try {
645
- const tomlContent = tomlStringify(config);
646
- const annotatedContent = `# Codex Configuration
647
- # Managed by Coding-Tool
648
- # Current provider: ${channel.name}
649
-
650
- ${tomlContent}`;
651
-
652
- fs.writeFileSync(configPath, annotatedContent, 'utf8');
653
- console.log(`[Codex Channels] Applied channel ${channel.name} to config.toml`);
654
- } catch (err) {
655
- console.error('[Codex Channels] Failed to write config with TOML stringify:', err);
656
- throw new Error('Failed to write config.toml: ' + err.message);
657
- }
601
+ writeAnnotatedCodexConfig(configPath, config, [
602
+ '# Codex Configuration',
603
+ '# Managed by Coding-Tool',
604
+ `# Current provider: ${channel.name}`
605
+ ]);
606
+ console.log(`[Codex Channels] Applied channel ${channel.name} to config.toml`);
658
607
 
659
608
  // 更新 auth.json
660
609
  let auth = {};
@@ -678,14 +627,7 @@ ${tomlContent}`;
678
627
 
679
628
  fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
680
629
 
681
- if (channel.apiKey && channel.envKey) {
682
- const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
683
- if (injectResult.success) {
684
- console.log(`[Codex Channels] Environment variable ${channel.envKey} injected`);
685
- }
686
- } else if (channel.envKey) {
687
- removeEnvFromShell(channel.envKey);
688
- }
630
+ // auth.json 已在上方写入 API Key,Codex 优先读取 auth.json,无需注入 shell
689
631
 
690
632
  return channel;
691
633
  }
@@ -705,6 +647,12 @@ function getEffectiveApiKey(channel) {
705
647
  return channel.apiKey || null;
706
648
  }
707
649
 
650
+ function disableAllChannels() {
651
+ const data = loadChannels();
652
+ data.channels.forEach(ch => { ch.enabled = false; });
653
+ saveChannels(data);
654
+ }
655
+
708
656
  module.exports = {
709
657
  getChannels,
710
658
  createChannel,
@@ -715,5 +663,6 @@ module.exports = {
715
663
  syncAllChannelEnvVars,
716
664
  writeCodexConfigForMultiChannel,
717
665
  applyChannelToSettings,
718
- getEffectiveApiKey
666
+ getEffectiveApiKey,
667
+ disableAllChannels
719
668
  };
@@ -1,16 +1,13 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const os = require('os');
4
3
  const toml = require('toml');
5
- const { resolvePreferredHomeDir } = require('../../utils/home-dir');
6
-
7
- const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
4
+ const { NATIVE_PATHS } = require('../../config/paths');
8
5
 
9
6
  /**
10
7
  * 获取 Codex 配置目录
11
8
  */
12
9
  function getCodexDir() {
13
- return path.join(HOME_DIR, '.codex');
10
+ return NATIVE_PATHS.codex.dir;
14
11
  }
15
12
 
16
13
  /**