@adversity/coding-tool-x 2.2.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 (125) hide show
  1. package/CHANGELOG.md +333 -0
  2. package/LICENSE +21 -0
  3. package/README.md +404 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
  6. package/dist/web/assets/index-aL3cKxSK.css +41 -0
  7. package/dist/web/favicon.ico +0 -0
  8. package/dist/web/index.html +14 -0
  9. package/dist/web/logo.png +0 -0
  10. package/docs/CHANGELOG.md +582 -0
  11. package/docs/DIRECTORY_MIGRATION.md +112 -0
  12. package/docs/PROJECT_STRUCTURE.md +396 -0
  13. package/docs/bannel.png +0 -0
  14. package/docs/home.png +0 -0
  15. package/docs/logo.png +0 -0
  16. package/docs/multi-channel-load-balancing.md +249 -0
  17. package/package.json +73 -0
  18. package/src/commands/channels.js +504 -0
  19. package/src/commands/cli-type.js +99 -0
  20. package/src/commands/daemon.js +286 -0
  21. package/src/commands/doctor.js +332 -0
  22. package/src/commands/list.js +222 -0
  23. package/src/commands/logs.js +259 -0
  24. package/src/commands/port-config.js +115 -0
  25. package/src/commands/proxy-control.js +258 -0
  26. package/src/commands/proxy.js +152 -0
  27. package/src/commands/resume.js +137 -0
  28. package/src/commands/search.js +190 -0
  29. package/src/commands/stats.js +224 -0
  30. package/src/commands/switch.js +48 -0
  31. package/src/commands/toggle-proxy.js +222 -0
  32. package/src/commands/ui.js +92 -0
  33. package/src/commands/workspace.js +454 -0
  34. package/src/config/default.js +40 -0
  35. package/src/config/loader.js +75 -0
  36. package/src/config/paths.js +121 -0
  37. package/src/index.js +373 -0
  38. package/src/reset-config.js +92 -0
  39. package/src/server/api/agents.js +248 -0
  40. package/src/server/api/aliases.js +36 -0
  41. package/src/server/api/channels.js +258 -0
  42. package/src/server/api/claude-hooks.js +480 -0
  43. package/src/server/api/codex-channels.js +312 -0
  44. package/src/server/api/codex-projects.js +91 -0
  45. package/src/server/api/codex-proxy.js +182 -0
  46. package/src/server/api/codex-sessions.js +491 -0
  47. package/src/server/api/codex-statistics.js +57 -0
  48. package/src/server/api/commands.js +245 -0
  49. package/src/server/api/config-templates.js +182 -0
  50. package/src/server/api/config.js +147 -0
  51. package/src/server/api/convert.js +127 -0
  52. package/src/server/api/dashboard.js +125 -0
  53. package/src/server/api/env.js +144 -0
  54. package/src/server/api/favorites.js +77 -0
  55. package/src/server/api/gemini-channels.js +261 -0
  56. package/src/server/api/gemini-projects.js +91 -0
  57. package/src/server/api/gemini-proxy.js +160 -0
  58. package/src/server/api/gemini-sessions.js +397 -0
  59. package/src/server/api/gemini-statistics.js +57 -0
  60. package/src/server/api/health-check.js +118 -0
  61. package/src/server/api/mcp.js +336 -0
  62. package/src/server/api/pm2-autostart.js +269 -0
  63. package/src/server/api/projects.js +124 -0
  64. package/src/server/api/prompts.js +279 -0
  65. package/src/server/api/proxy.js +235 -0
  66. package/src/server/api/rules.js +271 -0
  67. package/src/server/api/sessions.js +595 -0
  68. package/src/server/api/settings.js +61 -0
  69. package/src/server/api/skills.js +305 -0
  70. package/src/server/api/statistics.js +91 -0
  71. package/src/server/api/terminal.js +202 -0
  72. package/src/server/api/ui-config.js +64 -0
  73. package/src/server/api/workspaces.js +407 -0
  74. package/src/server/codex-proxy-server.js +538 -0
  75. package/src/server/dev-server.js +26 -0
  76. package/src/server/gemini-proxy-server.js +518 -0
  77. package/src/server/index.js +305 -0
  78. package/src/server/proxy-server.js +469 -0
  79. package/src/server/services/agents-service.js +354 -0
  80. package/src/server/services/alias.js +71 -0
  81. package/src/server/services/channel-health.js +234 -0
  82. package/src/server/services/channel-scheduler.js +234 -0
  83. package/src/server/services/channels.js +347 -0
  84. package/src/server/services/codex-channels.js +625 -0
  85. package/src/server/services/codex-config.js +90 -0
  86. package/src/server/services/codex-parser.js +322 -0
  87. package/src/server/services/codex-sessions.js +665 -0
  88. package/src/server/services/codex-settings-manager.js +397 -0
  89. package/src/server/services/codex-speed-test-template.json +24 -0
  90. package/src/server/services/codex-statistics-service.js +255 -0
  91. package/src/server/services/commands-service.js +360 -0
  92. package/src/server/services/config-templates-service.js +732 -0
  93. package/src/server/services/env-checker.js +307 -0
  94. package/src/server/services/env-manager.js +300 -0
  95. package/src/server/services/favorites.js +163 -0
  96. package/src/server/services/gemini-channels.js +333 -0
  97. package/src/server/services/gemini-config.js +73 -0
  98. package/src/server/services/gemini-sessions.js +689 -0
  99. package/src/server/services/gemini-settings-manager.js +263 -0
  100. package/src/server/services/gemini-statistics-service.js +253 -0
  101. package/src/server/services/health-check.js +399 -0
  102. package/src/server/services/mcp-service.js +1188 -0
  103. package/src/server/services/prompts-service.js +492 -0
  104. package/src/server/services/proxy-runtime.js +79 -0
  105. package/src/server/services/pty-manager.js +435 -0
  106. package/src/server/services/rules-service.js +401 -0
  107. package/src/server/services/session-cache.js +127 -0
  108. package/src/server/services/session-converter.js +577 -0
  109. package/src/server/services/sessions.js +757 -0
  110. package/src/server/services/settings-manager.js +163 -0
  111. package/src/server/services/skill-service.js +965 -0
  112. package/src/server/services/speed-test.js +545 -0
  113. package/src/server/services/statistics-service.js +386 -0
  114. package/src/server/services/terminal-commands.js +155 -0
  115. package/src/server/services/terminal-config.js +140 -0
  116. package/src/server/services/terminal-detector.js +306 -0
  117. package/src/server/services/ui-config.js +130 -0
  118. package/src/server/services/workspace-service.js +662 -0
  119. package/src/server/utils/pricing.js +41 -0
  120. package/src/server/websocket-server.js +557 -0
  121. package/src/ui/menu.js +129 -0
  122. package/src/ui/prompts.js +100 -0
  123. package/src/utils/format.js +43 -0
  124. package/src/utils/port-helper.js +94 -0
  125. package/src/utils/session.js +239 -0
@@ -0,0 +1,234 @@
1
+ const { getAllChannels } = require('./channels');
2
+ const { getChannels: getCodexChannels } = require('./codex-channels');
3
+ const { getChannels: getGeminiChannels } = require('./gemini-channels');
4
+ const { isChannelAvailable, getChannelHealthStatus, setOnChannelFrozen } = require('./channel-health');
5
+
6
+ const channelProviders = {
7
+ claude: () => getAllChannels(),
8
+ codex: () => {
9
+ const data = getCodexChannels();
10
+ return Array.isArray(data?.channels) ? data.channels : [];
11
+ },
12
+ gemini: () => {
13
+ const data = getGeminiChannels();
14
+ return Array.isArray(data?.channels) ? data.channels : [];
15
+ }
16
+ };
17
+
18
+ function createState() {
19
+ return {
20
+ channels: [],
21
+ inflight: new Map(),
22
+ sessionBindings: new Map(),
23
+ queue: []
24
+ };
25
+ }
26
+
27
+ const schedulerStates = {
28
+ claude: createState(),
29
+ codex: createState(),
30
+ gemini: createState()
31
+ };
32
+
33
+ function getState(source = 'claude') {
34
+ if (!schedulerStates[source]) {
35
+ schedulerStates[source] = createState();
36
+ }
37
+ return schedulerStates[source];
38
+ }
39
+
40
+ const WAIT_TIMEOUT_MS = 15000;
41
+
42
+ /**
43
+ * 解绑指定渠道的所有会话
44
+ */
45
+ function unbindChannelSessions(source, channelId) {
46
+ const state = getState(source);
47
+ let unbindCount = 0;
48
+ for (const [sessionId, boundChannelId] of state.sessionBindings) {
49
+ if (boundChannelId === channelId) {
50
+ state.sessionBindings.delete(sessionId);
51
+ unbindCount++;
52
+ }
53
+ }
54
+ if (unbindCount > 0) {
55
+ console.log(`[ChannelScheduler] Unbound ${unbindCount} sessions from ${source} channel ${channelId}`);
56
+ }
57
+ }
58
+
59
+ // 注册冻结回调,当渠道被冻结时解绑其会话
60
+ setOnChannelFrozen(unbindChannelSessions);
61
+
62
+ function refreshChannels(source = 'claude') {
63
+ const state = getState(source);
64
+ const provider = channelProviders[source];
65
+ if (!provider) return;
66
+
67
+ // 每次直接读取最新配置,不做缓存
68
+ const raw = provider();
69
+ state.channels = raw
70
+ .filter(ch => ch.enabled !== false)
71
+ .map(ch => ({
72
+ id: ch.id,
73
+ name: ch.name,
74
+ baseUrl: ch.baseUrl,
75
+ apiKey: ch.apiKey,
76
+ weight: Math.max(1, Number(ch.weight) || 1),
77
+ maxConcurrency: ch.maxConcurrency ?? null
78
+ }));
79
+
80
+ state.channels.forEach(ch => {
81
+ if (!state.inflight.has(ch.id)) {
82
+ state.inflight.set(ch.id, 0);
83
+ }
84
+ });
85
+ }
86
+
87
+ function getAvailableChannels(source = 'claude') {
88
+ refreshChannels(source);
89
+ const state = getState(source);
90
+ return state.channels.filter(ch => {
91
+ if (!isChannelAvailable(ch.id, source)) {
92
+ return false;
93
+ }
94
+ if (ch.maxConcurrency === null) {
95
+ return true;
96
+ }
97
+ return (state.inflight.get(ch.id) || 0) < ch.maxConcurrency;
98
+ });
99
+ }
100
+
101
+ function pickWeightedChannel(channels) {
102
+ if (!channels.length) return null;
103
+ const totalWeight = channels.reduce((sum, ch) => sum + ch.weight, 0);
104
+ let threshold = Math.random() * totalWeight;
105
+ for (const channel of channels) {
106
+ threshold -= channel.weight;
107
+ if (threshold <= 0) {
108
+ return channel;
109
+ }
110
+ }
111
+ return channels[channels.length - 1];
112
+ }
113
+
114
+ function tryAllocate(source = 'claude', options = {}) {
115
+ const state = getState(source);
116
+ const sessionId = options.sessionId;
117
+ const enableSessionBinding = options.enableSessionBinding !== false; // 默认开启
118
+ const available = getAvailableChannels(source);
119
+ if (!available.length) {
120
+ return null;
121
+ }
122
+
123
+ // 如果启用会话绑定且已有绑定,优先使用绑定渠道
124
+ if (enableSessionBinding && sessionId && state.sessionBindings.has(sessionId)) {
125
+ const boundId = state.sessionBindings.get(sessionId);
126
+ const boundChannel = available.find(ch => ch.id === boundId);
127
+ if (boundChannel) {
128
+ state.inflight.set(boundChannel.id, (state.inflight.get(boundChannel.id) || 0) + 1);
129
+ return boundChannel;
130
+ }
131
+ }
132
+
133
+ // 选择新的渠道(加权随机)
134
+ const chosen = pickWeightedChannel(available);
135
+ if (!chosen) return null;
136
+
137
+ // 只有在启用会话绑定时才记录绑定
138
+ if (enableSessionBinding && sessionId) {
139
+ state.sessionBindings.set(sessionId, chosen.id);
140
+ }
141
+ state.inflight.set(chosen.id, (state.inflight.get(chosen.id) || 0) + 1);
142
+ return chosen;
143
+ }
144
+
145
+ function drainQueue(source = 'claude') {
146
+ const state = getState(source);
147
+ if (!state.queue.length) return;
148
+
149
+ for (let i = 0; i < state.queue.length; i++) {
150
+ const entry = state.queue[i];
151
+ const channel = tryAllocate(source, entry.options);
152
+ if (channel) {
153
+ clearTimeout(entry.timer);
154
+ state.queue.splice(i, 1);
155
+ entry.resolve(channel);
156
+ return drainQueue(source);
157
+ }
158
+ }
159
+ }
160
+
161
+ function allocateChannel(options = {}) {
162
+ const source = options.source || 'claude';
163
+ const state = getState(source);
164
+ const channel = tryAllocate(source, options);
165
+ if (channel) {
166
+ return Promise.resolve(channel);
167
+ }
168
+
169
+ if (!state.channels.length) {
170
+ return Promise.reject(new Error('暂无可用渠道,请先添加并启用至少一个渠道'));
171
+ }
172
+
173
+ // 检查是否所有渠道都被冻结
174
+ const allFrozen = state.channels.every(ch => !isChannelAvailable(ch.id, source));
175
+
176
+ return new Promise((resolve, reject) => {
177
+ const timer = setTimeout(() => {
178
+ const index = state.queue.findIndex(item => item.timer === timer);
179
+ if (index !== -1) {
180
+ state.queue.splice(index, 1);
181
+ }
182
+ // 根据实际情况返回更准确的错误信息
183
+ if (allFrozen) {
184
+ reject(new Error('所有渠道均已被冻结,请等待健康检查恢复或手动重置'));
185
+ } else {
186
+ reject(new Error('所有渠道均已达到并发上限,请稍后重试'));
187
+ }
188
+ }, WAIT_TIMEOUT_MS);
189
+
190
+ state.queue.push({
191
+ source,
192
+ options,
193
+ resolve,
194
+ reject,
195
+ timer
196
+ });
197
+ });
198
+ }
199
+
200
+ function releaseChannel(channelId, source = 'claude') {
201
+ const state = getState(source);
202
+ if (!channelId) return;
203
+ if (!state.inflight.has(channelId)) {
204
+ state.inflight.set(channelId, 0);
205
+ }
206
+ const current = state.inflight.get(channelId) || 0;
207
+ state.inflight.set(channelId, current > 0 ? current - 1 : 0);
208
+ drainQueue(source);
209
+ }
210
+
211
+ function getSchedulerState(source = 'claude') {
212
+ refreshChannels(source);
213
+ const state = getState(source);
214
+ return {
215
+ channels: state.channels.map(ch => {
216
+ const healthStatus = getChannelHealthStatus(ch.id, source);
217
+ return {
218
+ id: ch.id,
219
+ name: ch.name,
220
+ weight: ch.weight,
221
+ maxConcurrency: ch.maxConcurrency,
222
+ inflight: state.inflight.get(ch.id) || 0,
223
+ health: healthStatus
224
+ };
225
+ }),
226
+ pending: state.queue.length
227
+ };
228
+ }
229
+
230
+ module.exports = {
231
+ allocateChannel,
232
+ releaseChannel,
233
+ getSchedulerState
234
+ };
@@ -0,0 +1,347 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { isProxyConfig } = require('./settings-manager');
5
+
6
+ function getChannelsFilePath() {
7
+ const dir = path.join(os.homedir(), '.claude', 'cc-tool');
8
+ if (!fs.existsSync(dir)) {
9
+ fs.mkdirSync(dir, { recursive: true });
10
+ }
11
+ return path.join(dir, 'channels.json');
12
+ }
13
+
14
+ function getActiveChannelIdPath() {
15
+ const dir = path.join(os.homedir(), '.claude', 'cc-tool');
16
+ if (!fs.existsSync(dir)) {
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ }
19
+ return path.join(dir, 'active-channel.json');
20
+ }
21
+
22
+ function getClaudeSettingsPath() {
23
+ return path.join(os.homedir(), '.claude', 'settings.json');
24
+ }
25
+
26
+ function saveActiveChannelId(channelId) {
27
+ const filePath = getActiveChannelIdPath();
28
+ fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
29
+ }
30
+
31
+ function loadActiveChannelId() {
32
+ const filePath = getActiveChannelIdPath();
33
+ try {
34
+ if (fs.existsSync(filePath)) {
35
+ const content = fs.readFileSync(filePath, 'utf8');
36
+ const data = JSON.parse(content);
37
+ return data.activeChannelId || null;
38
+ }
39
+ } catch (error) {
40
+ console.error('Error loading active channel ID:', error);
41
+ }
42
+ return null;
43
+ }
44
+
45
+ let channelsCache = null;
46
+ let channelsCacheInitialized = false;
47
+ const DEFAULT_CHANNELS = { channels: [] };
48
+
49
+ function normalizeNumber(value, defaultValue, max = null) {
50
+ const num = Number(value);
51
+ if (!Number.isFinite(num) || num <= 0) {
52
+ return defaultValue;
53
+ }
54
+ if (max !== null && num > max) {
55
+ return max;
56
+ }
57
+ return num;
58
+ }
59
+
60
+ function applyChannelDefaults(channel) {
61
+ const normalized = { ...channel };
62
+ if (normalized.enabled === undefined) {
63
+ normalized.enabled = true;
64
+ } else {
65
+ normalized.enabled = !!normalized.enabled;
66
+ }
67
+
68
+ normalized.weight = normalizeNumber(normalized.weight, 1, 100);
69
+
70
+ if (normalized.maxConcurrency === undefined ||
71
+ normalized.maxConcurrency === null ||
72
+ normalized.maxConcurrency === 0) {
73
+ normalized.maxConcurrency = null;
74
+ } else {
75
+ normalized.maxConcurrency = normalizeNumber(normalized.maxConcurrency, 1, 100);
76
+ }
77
+
78
+ return normalized;
79
+ }
80
+
81
+ function readChannelsFromFile() {
82
+ const filePath = getChannelsFilePath();
83
+ try {
84
+ if (fs.existsSync(filePath)) {
85
+ const content = fs.readFileSync(filePath, 'utf8');
86
+ const data = JSON.parse(content);
87
+ data.channels = (data.channels || []).map(applyChannelDefaults);
88
+ return data;
89
+ }
90
+ } catch (error) {
91
+ console.error('Error loading channels:', error);
92
+ }
93
+ return { ...DEFAULT_CHANNELS };
94
+ }
95
+
96
+ function initializeChannelsCache() {
97
+ if (channelsCacheInitialized) return;
98
+ channelsCache = readChannelsFromFile();
99
+ channelsCacheInitialized = true;
100
+
101
+ try {
102
+ const filePath = getChannelsFilePath();
103
+ fs.watchFile(filePath, { persistent: false }, () => {
104
+ channelsCache = readChannelsFromFile();
105
+ });
106
+ } catch (err) {
107
+ console.error('Failed to watch channels file:', err);
108
+ }
109
+ }
110
+
111
+ function loadChannels() {
112
+ if (!channelsCacheInitialized) {
113
+ initializeChannelsCache();
114
+ }
115
+ return JSON.parse(JSON.stringify(channelsCache));
116
+ }
117
+
118
+ function saveChannels(data) {
119
+ const filePath = getChannelsFilePath();
120
+ const payload = {
121
+ ...data,
122
+ channels: (data.channels || []).map(applyChannelDefaults)
123
+ };
124
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
125
+ channelsCache = JSON.parse(JSON.stringify(payload));
126
+ }
127
+
128
+ function getCurrentSettings() {
129
+ try {
130
+ const settingsPath = getClaudeSettingsPath();
131
+ if (!fs.existsSync(settingsPath)) {
132
+ return null;
133
+ }
134
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
135
+
136
+ let baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
137
+ let apiKey = settings.env?.ANTHROPIC_API_KEY ||
138
+ settings.env?.ANTHROPIC_AUTH_TOKEN ||
139
+ '';
140
+
141
+ if (!apiKey && settings.apiKeyHelper) {
142
+ const match = settings.apiKeyHelper.match(/['"]([^'"]+)['"]/);
143
+ if (match && match[1]) {
144
+ apiKey = match[1];
145
+ }
146
+ }
147
+
148
+ if (!baseUrl && !apiKey) {
149
+ return null;
150
+ }
151
+
152
+ return { baseUrl, apiKey };
153
+ } catch (error) {
154
+ console.error('Error reading current settings:', error);
155
+ return null;
156
+ }
157
+ }
158
+
159
+ function getBestChannelForRestore() {
160
+ const data = loadChannels();
161
+ const enabledChannels = data.channels.filter(ch => ch.enabled !== false);
162
+
163
+ if (enabledChannels.length === 0) {
164
+ return data.channels[0];
165
+ }
166
+
167
+ enabledChannels.sort((a, b) => (b.weight || 1) - (a.weight || 1));
168
+ return enabledChannels[0];
169
+ }
170
+
171
+ function getAllChannels() {
172
+ const data = loadChannels();
173
+ return data.channels;
174
+ }
175
+
176
+ function createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig = {}) {
177
+ const data = loadChannels();
178
+ const newChannel = applyChannelDefaults({
179
+ id: `channel-${Date.now()}`,
180
+ name,
181
+ baseUrl,
182
+ apiKey,
183
+ createdAt: Date.now(),
184
+ websiteUrl: websiteUrl || undefined,
185
+ enabled: extraConfig.enabled !== undefined ? !!extraConfig.enabled : true,
186
+ weight: extraConfig.weight,
187
+ maxConcurrency: extraConfig.maxConcurrency,
188
+ presetId: extraConfig.presetId || 'official',
189
+ modelConfig: extraConfig.modelConfig || null,
190
+ proxyUrl: extraConfig.proxyUrl || ''
191
+ });
192
+
193
+ data.channels.push(newChannel);
194
+ saveChannels(data);
195
+ return newChannel;
196
+ }
197
+
198
+ function updateChannel(id, updates) {
199
+ const data = loadChannels();
200
+ const index = data.channels.findIndex(ch => ch.id === id);
201
+
202
+ if (index === -1) {
203
+ throw new Error('Channel not found');
204
+ }
205
+
206
+ const merged = { ...data.channels[index], ...updates };
207
+ data.channels[index] = applyChannelDefaults({
208
+ ...merged,
209
+ weight: merged.weight,
210
+ maxConcurrency: merged.maxConcurrency,
211
+ enabled: merged.enabled,
212
+ presetId: merged.presetId,
213
+ modelConfig: merged.modelConfig,
214
+ proxyUrl: merged.proxyUrl
215
+ });
216
+
217
+ saveChannels(data);
218
+ return data.channels[index];
219
+ }
220
+
221
+ function deleteChannel(id) {
222
+ const data = loadChannels();
223
+ const index = data.channels.findIndex(ch => ch.id === id);
224
+
225
+ if (index === -1) {
226
+ throw new Error('Channel not found');
227
+ }
228
+
229
+ data.channels.splice(index, 1);
230
+ saveChannels(data);
231
+ return { success: true };
232
+ }
233
+
234
+ function applyChannelToSettings(id) {
235
+ const data = loadChannels();
236
+ const channel = data.channels.find(ch => ch.id === id);
237
+
238
+ if (!channel) {
239
+ throw new Error('Channel not found');
240
+ }
241
+
242
+ channel.enabled = true;
243
+ saveChannels(data);
244
+ updateClaudeSettingsWithModelConfig(channel);
245
+
246
+ return channel;
247
+ }
248
+
249
+ function updateClaudeSettingsWithModelConfig(channel) {
250
+ const settingsPath = getClaudeSettingsPath();
251
+
252
+ let settings = {};
253
+ if (fs.existsSync(settingsPath)) {
254
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
255
+ }
256
+
257
+ if (!settings.env) {
258
+ settings.env = {};
259
+ }
260
+
261
+ const { baseUrl, apiKey, modelConfig, presetId, proxyUrl } = channel;
262
+
263
+ const useAuthToken = settings.env.ANTHROPIC_AUTH_TOKEN !== undefined;
264
+ const useApiKey = settings.env.ANTHROPIC_API_KEY !== undefined;
265
+
266
+ settings.env.ANTHROPIC_BASE_URL = baseUrl;
267
+
268
+ if (useAuthToken || (!useAuthToken && !useApiKey)) {
269
+ settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
270
+ delete settings.env.ANTHROPIC_API_KEY;
271
+ } else {
272
+ settings.env.ANTHROPIC_API_KEY = apiKey;
273
+ }
274
+
275
+ if (presetId && presetId !== 'official' && modelConfig) {
276
+ if (modelConfig.model) {
277
+ settings.env.ANTHROPIC_MODEL = modelConfig.model;
278
+ }
279
+ if (modelConfig.haikuModel) {
280
+ settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = modelConfig.haikuModel;
281
+ }
282
+ if (modelConfig.sonnetModel) {
283
+ settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL = modelConfig.sonnetModel;
284
+ }
285
+ if (modelConfig.opusModel) {
286
+ settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = modelConfig.opusModel;
287
+ }
288
+ } else {
289
+ delete settings.env.ANTHROPIC_MODEL;
290
+ delete settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
291
+ delete settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
292
+ delete settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL;
293
+ }
294
+
295
+ if (proxyUrl) {
296
+ settings.env.HTTPS_PROXY = proxyUrl;
297
+ settings.env.HTTP_PROXY = proxyUrl;
298
+ } else {
299
+ delete settings.env.HTTPS_PROXY;
300
+ delete settings.env.HTTP_PROXY;
301
+ delete settings.env.NO_PROXY;
302
+ }
303
+
304
+ settings.apiKeyHelper = `echo '${apiKey}'`;
305
+
306
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
307
+ }
308
+
309
+ function updateClaudeSettings(baseUrl, apiKey) {
310
+ const settingsPath = getClaudeSettingsPath();
311
+
312
+ let settings = {};
313
+ if (fs.existsSync(settingsPath)) {
314
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
315
+ }
316
+
317
+ if (!settings.env) {
318
+ settings.env = {};
319
+ }
320
+
321
+ const useAuthToken = settings.env.ANTHROPIC_AUTH_TOKEN !== undefined;
322
+ const useApiKey = settings.env.ANTHROPIC_API_KEY !== undefined;
323
+
324
+ settings.env.ANTHROPIC_BASE_URL = baseUrl;
325
+
326
+ if (useAuthToken || (!useAuthToken && !useApiKey)) {
327
+ settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
328
+ delete settings.env.ANTHROPIC_API_KEY;
329
+ } else {
330
+ settings.env.ANTHROPIC_API_KEY = apiKey;
331
+ }
332
+
333
+ settings.apiKeyHelper = `echo '${apiKey}'`;
334
+
335
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
336
+ }
337
+
338
+ module.exports = {
339
+ getAllChannels,
340
+ getCurrentSettings,
341
+ createChannel,
342
+ updateChannel,
343
+ deleteChannel,
344
+ applyChannelToSettings,
345
+ getBestChannelForRestore,
346
+ updateClaudeSettings
347
+ };