@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,469 @@
1
+ const express = require('express');
2
+ const httpProxy = require('http-proxy');
3
+ const http = require('http');
4
+ const chalk = require('chalk');
5
+ const { HttpsProxyAgent } = require('https-proxy-agent');
6
+ const { allocateChannel, releaseChannel, getSchedulerState } = require('./services/channel-scheduler');
7
+ const { recordSuccess, recordFailure } = require('./services/channel-health');
8
+ const { broadcastLog, broadcastSchedulerState } = require('./websocket-server');
9
+ const { loadConfig } = require('../config/loader');
10
+ const DEFAULT_CONFIG = require('../config/default');
11
+ const { resolvePricing } = require('./utils/pricing');
12
+ const { recordRequest } = require('./services/statistics-service');
13
+ const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
14
+
15
+ let proxyServer = null;
16
+ let proxyApp = null;
17
+ let currentPort = null;
18
+
19
+ // 用于存储每个请求的元数据(用于 WebSocket 日志)
20
+ const requestMetadata = new Map();
21
+
22
+ // Claude API 定价(每百万 tokens 的价格,单位:美元)
23
+ const PRICING = {
24
+ 'claude-sonnet-4-5-20250929': { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.30 },
25
+ 'claude-sonnet-4-20250514': { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.30 },
26
+ 'claude-sonnet-3-5-20241022': { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.30 },
27
+ 'claude-sonnet-3-5-20240620': { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.30 },
28
+ 'claude-opus-4-20250514': { input: 15, output: 75, cacheCreation: 18.75, cacheRead: 1.50 },
29
+ 'claude-opus-3-20240229': { input: 15, output: 75, cacheCreation: 18.75, cacheRead: 1.50 },
30
+ 'claude-haiku-3-5-20241022': { input: 0.8, output: 4, cacheCreation: 1, cacheRead: 0.08 },
31
+ 'claude-3-5-haiku-20241022': { input: 0.8, output: 4, cacheCreation: 1, cacheRead: 0.08 }
32
+ };
33
+
34
+ const CLAUDE_BASE_PRICING = DEFAULT_CONFIG.pricing.claude;
35
+ const ONE_MILLION = 1000000;
36
+
37
+ /**
38
+ * 计算请求成本
39
+ * @param {string} model - 模型名称
40
+ * @param {object} tokens - token 使用情况
41
+ * @returns {number} 成本(美元)
42
+ */
43
+ function calculateCost(model, tokens) {
44
+ const basePricing = PRICING[model] || {};
45
+ const pricing = resolvePricing('claude', basePricing, CLAUDE_BASE_PRICING);
46
+
47
+ const inputRate = typeof pricing.input === 'number' ? pricing.input : CLAUDE_BASE_PRICING.input;
48
+ const outputRate = typeof pricing.output === 'number' ? pricing.output : CLAUDE_BASE_PRICING.output;
49
+ const cacheCreationRate = typeof pricing.cacheCreation === 'number' ? pricing.cacheCreation : CLAUDE_BASE_PRICING.cacheCreation;
50
+ const cacheReadRate = typeof pricing.cacheRead === 'number' ? pricing.cacheRead : CLAUDE_BASE_PRICING.cacheRead;
51
+
52
+ return (
53
+ (tokens.input || 0) * inputRate / ONE_MILLION +
54
+ (tokens.output || 0) * outputRate / ONE_MILLION +
55
+ (tokens.cacheCreation || 0) * cacheCreationRate / ONE_MILLION +
56
+ (tokens.cacheRead || 0) * cacheReadRate / ONE_MILLION
57
+ );
58
+ }
59
+
60
+ const jsonBodyParser = express.json({
61
+ limit: '100mb',
62
+ verify: (req, res, buf) => {
63
+ req.rawBody = Buffer.from(buf);
64
+ }
65
+ });
66
+
67
+ function shouldParseJson(req) {
68
+ const contentType = req.headers['content-type'] || '';
69
+ return req.method === 'POST' && contentType.includes('application/json');
70
+ }
71
+
72
+ function extractSessionIdFromBody(body = {}) {
73
+ if (!body || typeof body !== 'object') return null;
74
+ return (
75
+ body.session_id ||
76
+ body.sessionId ||
77
+ body.conversation_id ||
78
+ body.conversationId ||
79
+ body.metadata?.session_id ||
80
+ body.metadata?.sessionId ||
81
+ body.metadata?.conversation_id ||
82
+ body.workspace?.workspace_id ||
83
+ body.project_id ||
84
+ null
85
+ );
86
+ }
87
+
88
+ function extractSessionId(req) {
89
+ const headerSession =
90
+ req.headers['x-session-id'] ||
91
+ req.headers['x-claude-session'] ||
92
+ req.headers['x-cc-session'];
93
+ if (headerSession) return String(headerSession);
94
+ if (req.body) {
95
+ return extractSessionIdFromBody(req.body);
96
+ }
97
+ return null;
98
+ }
99
+
100
+ async function startProxyServer(options = {}) {
101
+ const preserveStartTime = options.preserveStartTime || false;
102
+
103
+ if (proxyServer) {
104
+ console.log('Proxy server already running on port', currentPort);
105
+ return { success: true, port: currentPort };
106
+ }
107
+
108
+ try {
109
+ const config = loadConfig();
110
+ const port = config.ports?.proxy || 10088;
111
+ currentPort = port;
112
+
113
+ proxyApp = express();
114
+
115
+ proxyApp.use((req, res, next) => {
116
+ if (shouldParseJson(req)) {
117
+ return jsonBodyParser(req, res, next);
118
+ }
119
+ return next();
120
+ });
121
+ const proxy = httpProxy.createProxyServer({});
122
+
123
+ proxy.on('proxyReq', (proxyReq, req, res) => {
124
+ const selectedChannel = req.selectedChannel;
125
+ if (selectedChannel) {
126
+ const requestId = `${Date.now()}-${Math.random()}`;
127
+ requestMetadata.set(req, {
128
+ id: requestId,
129
+ channel: selectedChannel.name,
130
+ channelId: selectedChannel.id,
131
+ startTime: Date.now(),
132
+ sessionId: req.sessionId || null
133
+ });
134
+
135
+ proxyReq.removeHeader('x-api-key');
136
+ proxyReq.setHeader('x-api-key', selectedChannel.apiKey);
137
+ proxyReq.removeHeader('authorization');
138
+ proxyReq.setHeader('authorization', `Bearer ${selectedChannel.apiKey}`);
139
+
140
+ if (!proxyReq.getHeader('anthropic-version')) {
141
+ proxyReq.setHeader('anthropic-version', '2023-06-01');
142
+ }
143
+ if (!proxyReq.getHeader('content-type')) {
144
+ proxyReq.setHeader('content-type', 'application/json');
145
+ }
146
+ }
147
+
148
+ if (shouldParseJson(req) && (req.rawBody || req.body)) {
149
+ const bodyBuffer = req.rawBody
150
+ ? Buffer.isBuffer(req.rawBody) ? req.rawBody : Buffer.from(req.rawBody)
151
+ : Buffer.from(JSON.stringify(req.body));
152
+ proxyReq.setHeader('Content-Length', bodyBuffer.length);
153
+ proxyReq.write(bodyBuffer);
154
+ proxyReq.end();
155
+ }
156
+ });
157
+
158
+ proxyApp.use(async (req, res) => {
159
+ try {
160
+ const sessionId = extractSessionId(req);
161
+ const config = loadConfig();
162
+ const enableSessionBinding = config.enableSessionBinding !== false; // 默认开启
163
+ const channel = await allocateChannel({ source: 'claude', sessionId, enableSessionBinding });
164
+
165
+ // 广播调度状态(请求开始)
166
+ broadcastSchedulerState('claude', getSchedulerState('claude'));
167
+
168
+ req.selectedChannel = channel;
169
+ req.sessionId = sessionId || null;
170
+ let released = false;
171
+
172
+ const release = () => {
173
+ if (released) return;
174
+ released = true;
175
+ releaseChannel(channel.id, 'claude');
176
+ // 广播调度状态(请求结束)
177
+ broadcastSchedulerState('claude', getSchedulerState('claude'));
178
+ };
179
+
180
+ req.__releaseChannel = release;
181
+
182
+ res.on('close', release);
183
+ res.on('error', release);
184
+
185
+ const proxyOptions = {
186
+ target: channel.baseUrl,
187
+ changeOrigin: true,
188
+ proxyTimeout: 120000, // 代理连接超时 2 分钟
189
+ timeout: 120000 // 请求超时 2 分钟
190
+ };
191
+
192
+ if (channel.proxyUrl) {
193
+ proxyOptions.agent = new HttpsProxyAgent(channel.proxyUrl);
194
+ }
195
+
196
+ proxy.web(req, res, proxyOptions, (err) => {
197
+ release();
198
+ if (err) {
199
+ // 记录请求失败
200
+ recordFailure(channel.id, 'claude', err);
201
+ console.error('Proxy error:', err);
202
+ if (res && !res.headersSent) {
203
+ res.status(502).json({
204
+ error: 'Proxy error: ' + err.message,
205
+ type: 'proxy_error'
206
+ });
207
+ }
208
+ }
209
+ });
210
+ } catch (error) {
211
+ console.error('Channel allocation error:', error);
212
+ if (!res.headersSent) {
213
+ res.status(503).json({
214
+ error: error.message || '所有渠道暂时不可用',
215
+ type: 'channel_pool_exhausted'
216
+ });
217
+ }
218
+ }
219
+ });
220
+
221
+ proxy.on('proxyRes', (proxyRes, req, res) => {
222
+ const metadata = requestMetadata.get(req);
223
+ if (!metadata) return;
224
+
225
+ if (res.writableEnded || res.destroyed) {
226
+ requestMetadata.delete(req);
227
+ return;
228
+ }
229
+
230
+ let isResponseClosed = false;
231
+
232
+ res.on('close', () => {
233
+ isResponseClosed = true;
234
+ requestMetadata.delete(req);
235
+ });
236
+
237
+ res.on('error', (err) => {
238
+ isResponseClosed = true;
239
+ if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
240
+ console.error('Response error:', err);
241
+ }
242
+ requestMetadata.delete(req);
243
+ });
244
+
245
+ let buffer = '';
246
+ let tokenData = {
247
+ inputTokens: 0,
248
+ outputTokens: 0,
249
+ cacheCreation: 0,
250
+ cacheRead: 0,
251
+ model: ''
252
+ };
253
+
254
+ proxyRes.on('data', (chunk) => {
255
+ if (isResponseClosed) return;
256
+
257
+ buffer += chunk.toString();
258
+
259
+ const events = buffer.split('\n\n');
260
+ buffer = events.pop() || '';
261
+
262
+ events.forEach(eventText => {
263
+ if (!eventText.trim()) return;
264
+
265
+ try {
266
+ const lines = eventText.split('\n');
267
+ let eventType = '';
268
+ let data = '';
269
+
270
+ lines.forEach(line => {
271
+ if (line.startsWith('event:')) {
272
+ eventType = line.substring(6).trim();
273
+ } else if (line.startsWith('data:')) {
274
+ data = line.substring(5).trim();
275
+ }
276
+ });
277
+
278
+ if (!data) return;
279
+
280
+ const parsed = JSON.parse(data);
281
+
282
+ if (eventType === 'message_start' && parsed.message && parsed.message.model) {
283
+ tokenData.model = parsed.message.model;
284
+ }
285
+
286
+ if (parsed.usage) {
287
+ if (parsed.usage.input_tokens !== undefined) {
288
+ tokenData.inputTokens = parsed.usage.input_tokens;
289
+ }
290
+ if (parsed.usage.output_tokens !== undefined) {
291
+ tokenData.outputTokens = parsed.usage.output_tokens;
292
+ }
293
+ if (parsed.usage.cache_creation_input_tokens !== undefined) {
294
+ tokenData.cacheCreation = parsed.usage.cache_creation_input_tokens;
295
+ }
296
+ if (parsed.usage.cache_read_input_tokens !== undefined) {
297
+ tokenData.cacheRead = parsed.usage.cache_read_input_tokens;
298
+ }
299
+ }
300
+
301
+ if (eventType === 'message_delta' && parsed.usage) {
302
+ const now = new Date();
303
+ const time = now.toLocaleTimeString('zh-CN', {
304
+ hour12: false,
305
+ hour: '2-digit',
306
+ minute: '2-digit',
307
+ second: '2-digit'
308
+ });
309
+
310
+ const tokens = {
311
+ input: tokenData.inputTokens,
312
+ output: tokenData.outputTokens,
313
+ cacheCreation: tokenData.cacheCreation,
314
+ cacheRead: tokenData.cacheRead,
315
+ total: tokenData.inputTokens + tokenData.outputTokens + tokenData.cacheCreation + tokenData.cacheRead
316
+ };
317
+ const cost = calculateCost(tokenData.model, tokens);
318
+
319
+ if (!isResponseClosed) {
320
+ broadcastLog({
321
+ type: 'log',
322
+ id: metadata.id,
323
+ time: time,
324
+ channel: metadata.channel,
325
+ model: tokenData.model,
326
+ inputTokens: tokenData.inputTokens,
327
+ outputTokens: tokenData.outputTokens,
328
+ cacheCreation: tokenData.cacheCreation,
329
+ cacheRead: tokenData.cacheRead,
330
+ cost: cost,
331
+ source: 'claude'
332
+ });
333
+ }
334
+
335
+ const duration = Date.now() - metadata.startTime;
336
+
337
+ recordRequest({
338
+ id: metadata.id,
339
+ timestamp: new Date(metadata.startTime).toISOString(),
340
+ toolType: 'claude-code',
341
+ channel: metadata.channel,
342
+ channelId: metadata.channelId,
343
+ model: tokenData.model,
344
+ tokens: tokens,
345
+ duration: duration,
346
+ success: true,
347
+ cost: cost
348
+ });
349
+
350
+ // 记录请求成功(用于健康检查)
351
+ recordSuccess(metadata.channelId, 'claude');
352
+ }
353
+ } catch (err) {
354
+ }
355
+ });
356
+ });
357
+
358
+ const finalize = () => {
359
+ if (!isResponseClosed) {
360
+ requestMetadata.delete(req);
361
+ }
362
+ if (typeof req.__releaseChannel === 'function') {
363
+ req.__releaseChannel();
364
+ }
365
+ };
366
+
367
+ proxyRes.on('end', finalize);
368
+
369
+ proxyRes.on('error', (err) => {
370
+ if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
371
+ console.error('Proxy response error:', err);
372
+ }
373
+ // 记录响应错误
374
+ if (metadata && metadata.channelId) {
375
+ recordFailure(metadata.channelId, 'claude', err);
376
+ }
377
+ isResponseClosed = true;
378
+ finalize();
379
+ });
380
+ });
381
+
382
+ proxy.on('error', (err, req, res) => {
383
+ console.error('Proxy error:', err);
384
+ // 记录请求失败(用于健康检查)
385
+ if (req && req.selectedChannel && req.selectedChannel.id) {
386
+ recordFailure(req.selectedChannel.id, 'claude', err);
387
+ }
388
+ if (res && !res.headersSent) {
389
+ res.status(502).json({
390
+ error: 'Proxy error: ' + err.message,
391
+ type: 'proxy_error'
392
+ });
393
+ }
394
+ });
395
+
396
+ proxyServer = http.createServer(proxyApp);
397
+
398
+ return new Promise((resolve, reject) => {
399
+ proxyServer.listen(port, '127.0.0.1', () => {
400
+ console.log(`✅ Proxy server started on http://127.0.0.1:${port}`);
401
+ saveProxyStartTime('claude', preserveStartTime);
402
+ resolve({ success: true, port });
403
+ });
404
+
405
+ proxyServer.on('error', (err) => {
406
+ if (err.code === 'EADDRINUSE') {
407
+ console.error(chalk.red(`\n❌ 代理服务端口 ${port} 已被占用`));
408
+ console.error(chalk.yellow('\n💡 解决方案:'));
409
+ console.error(chalk.gray(' 1. 运行 ctx 命令,选择"配置端口"修改端口'));
410
+ console.error(chalk.gray(` 2. 或关闭占用端口 ${port} 的程序\n`));
411
+ } else {
412
+ console.error('Failed to start proxy server:', err);
413
+ }
414
+ proxyServer = null;
415
+ proxyApp = null;
416
+ currentPort = null;
417
+ reject(err);
418
+ });
419
+ });
420
+ } catch (err) {
421
+ console.error('Error starting proxy server:', err);
422
+ throw err;
423
+ }
424
+ }
425
+
426
+ async function stopProxyServer(options = {}) {
427
+ const clearStartTime = options.clearStartTime !== false;
428
+
429
+ if (!proxyServer) {
430
+ return { success: true, message: 'Proxy server not running' };
431
+ }
432
+
433
+ requestMetadata.clear();
434
+
435
+ return new Promise((resolve) => {
436
+ proxyServer.close(() => {
437
+ console.log('✅ Proxy server stopped');
438
+ if (clearStartTime) {
439
+ clearProxyStartTime('claude');
440
+ }
441
+ proxyServer = null;
442
+ proxyApp = null;
443
+ const stoppedPort = currentPort;
444
+ currentPort = null;
445
+ resolve({ success: true, port: stoppedPort });
446
+ });
447
+ });
448
+ }
449
+
450
+ // 获取代理服务器状态
451
+ function getProxyStatus() {
452
+ const config = loadConfig();
453
+ const startTime = getProxyStartTime('claude');
454
+ const runtime = getProxyRuntime('claude');
455
+
456
+ return {
457
+ running: !!proxyServer,
458
+ port: currentPort,
459
+ defaultPort: config.ports?.proxy || 10088,
460
+ startTime,
461
+ runtime
462
+ };
463
+ }
464
+
465
+ module.exports = {
466
+ startProxyServer,
467
+ stopProxyServer,
468
+ getProxyStatus
469
+ };