@adversity/coding-tool-x 3.0.5 → 3.0.6

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.
@@ -5,12 +5,12 @@
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>CC-TOOL - ClaudeCode增强工作助手</title>
8
- <script type="module" crossorigin src="/assets/index-B4w1yh7H.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-D2VfwJBa.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-6JaYHOiI.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendors-D2HHw_aW.js">
11
- <link rel="modulepreload" crossorigin href="/assets/icons-BlzwYoRU.js">
12
- <link rel="modulepreload" crossorigin href="/assets/naive-ui-B1TP-0TP.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-19ZPjh5b.css">
11
+ <link rel="modulepreload" crossorigin href="/assets/icons-BxudHPiX.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/naive-ui-DT-Uur8K.js">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-oXBzu0bd.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adversity/coding-tool-x",
3
- "version": "3.0.5",
3
+ "version": "3.0.6",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -15,6 +15,7 @@ const { getChannelHealthStatus, getAllChannelHealthStatus, resetChannelHealth }
15
15
  const { testChannelSpeed, testMultipleChannels, getLatencyLevel } = require('../services/speed-test');
16
16
  const { fetchModelsFromProvider } = require('../services/model-detector');
17
17
  const { broadcastLog, broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
18
+ const { clearRedirectCache } = require('../proxy-server');
18
19
 
19
20
  // GET /api/channels - Get all channels with health status
20
21
  router.get('/', (req, res) => {
@@ -102,6 +103,8 @@ router.put('/:id', (req, res) => {
102
103
  const updates = req.body;
103
104
 
104
105
  const channel = updateChannel(id, updates);
106
+ // 清除该渠道的模型重定向日志缓存,使下次请求时重新打印
107
+ clearRedirectCache(id);
105
108
  res.json({ channel });
106
109
  broadcastSchedulerState('claude', getSchedulerState('claude'));
107
110
  } catch (error) {
@@ -14,6 +14,8 @@ const { getChannelHealthStatus, resetChannelHealth } = require('../services/chan
14
14
  const { broadcastSchedulerState, broadcastLog } = require('../websocket-server');
15
15
  const { isCodexInstalled } = require('../services/codex-config');
16
16
  const { testChannelSpeed, testMultipleChannels, getLatencyLevel } = require('../services/speed-test');
17
+ const { clearCodexRedirectCache } = require('../codex-proxy-server');
18
+ const { fetchModelsFromProvider } = require('../services/model-detector');
17
19
 
18
20
  module.exports = (config) => {
19
21
  /**
@@ -42,6 +44,42 @@ module.exports = (config) => {
42
44
  }
43
45
  });
44
46
 
47
+ /**
48
+ * GET /api/codex/channels/:id/models
49
+ * 获取渠道可用模型列表
50
+ */
51
+ router.get('/:id/models', async (req, res) => {
52
+ try {
53
+ const { id } = req.params;
54
+ const channels = getChannels().channels || [];
55
+ const channel = channels.find(ch => ch.id === id);
56
+
57
+ if (!channel) {
58
+ return res.status(404).json({ error: '渠道不存在' });
59
+ }
60
+
61
+ // Codex 渠道大多数是 OpenAI 兼容的
62
+ const result = await fetchModelsFromProvider(channel, 'openai_compatible');
63
+
64
+ res.json({
65
+ channelId: id,
66
+ models: result.models,
67
+ supported: result.supported,
68
+ cached: result.cached,
69
+ fallbackUsed: result.fallbackUsed,
70
+ fetchedAt: result.lastChecked || new Date().toISOString(),
71
+ error: result.error,
72
+ errorHint: result.errorHint
73
+ });
74
+ } catch (error) {
75
+ console.error('[Codex Channels API] Error fetching models:', error);
76
+ res.status(500).json({
77
+ error: '获取模型列表失败',
78
+ channelId: req.params.id
79
+ });
80
+ }
81
+ });
82
+
45
83
  /**
46
84
  * POST /api/codex/channels
47
85
  * 创建新渠道
@@ -88,6 +126,8 @@ module.exports = (config) => {
88
126
  const updates = req.body;
89
127
 
90
128
  const channel = updateChannel(channelId, updates);
129
+ // 清除该渠道的模型重定向日志缓存,使下次请求时重新打印
130
+ clearCodexRedirectCache(channelId);
91
131
  res.json(channel);
92
132
  broadcastSchedulerState('codex', getSchedulerState('codex'));
93
133
  } catch (err) {
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Config Registry API 路由
3
+ *
4
+ * Exposes config registry functionality via REST API.
5
+ * Manages skills, commands, agents, rules with enable/disable and per-platform support.
6
+ */
7
+
8
+ const express = require('express');
9
+ const { ConfigRegistryService, CONFIG_TYPES } = require('../services/config-registry-service');
10
+ const { ConfigSyncManager } = require('../services/config-sync-manager');
11
+
12
+ const router = express.Router();
13
+ const registryService = new ConfigRegistryService();
14
+ const syncManager = new ConfigSyncManager();
15
+
16
+ // Valid config types
17
+ const VALID_TYPES = CONFIG_TYPES;
18
+
19
+ // Valid platforms
20
+ const VALID_PLATFORMS = ['claude', 'codex'];
21
+
22
+ /**
23
+ * Validate config type parameter
24
+ * @param {string} type - Config type
25
+ * @returns {string|null} Error message or null if valid
26
+ */
27
+ function validateType(type) {
28
+ if (!VALID_TYPES.includes(type)) {
29
+ return `Invalid config type: ${type}. Must be one of: ${VALID_TYPES.join(', ')}`;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * Validate platform parameter
36
+ * @param {string} platform - Platform name
37
+ * @returns {string|null} Error message or null if valid
38
+ */
39
+ function validatePlatform(platform) {
40
+ if (!VALID_PLATFORMS.includes(platform)) {
41
+ return `Invalid platform: ${platform}. Must be one of: ${VALID_PLATFORMS.join(', ')}`;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * GET /api/config-registry/stats
48
+ * Get statistics for all config types
49
+ */
50
+ router.get('/stats', async (req, res) => {
51
+ try {
52
+ const stats = registryService.getStats();
53
+
54
+ res.json({
55
+ success: true,
56
+ stats
57
+ });
58
+ } catch (err) {
59
+ console.error('[ConfigRegistry API] Get stats error:', err);
60
+ res.status(500).json({
61
+ success: false,
62
+ message: err.message
63
+ });
64
+ }
65
+ });
66
+
67
+ /**
68
+ * GET /api/config-registry/:type
69
+ * List all items for a config type (skills, commands, agents, rules)
70
+ * Returns { success: true, items: { name: registryEntry } }
71
+ */
72
+ router.get('/:type', async (req, res) => {
73
+ try {
74
+ const { type } = req.params;
75
+
76
+ const typeError = validateType(type);
77
+ if (typeError) {
78
+ return res.status(400).json({
79
+ success: false,
80
+ message: typeError
81
+ });
82
+ }
83
+
84
+ const items = registryService.listItems(type);
85
+
86
+ res.json({
87
+ success: true,
88
+ type,
89
+ items
90
+ });
91
+ } catch (err) {
92
+ console.error('[ConfigRegistry API] List items error:', err);
93
+ res.status(500).json({
94
+ success: false,
95
+ message: err.message
96
+ });
97
+ }
98
+ });
99
+
100
+ /**
101
+ * POST /api/config-registry/:type/import
102
+ * Import configs from Claude Code native directories to cc-tool
103
+ * - Scans ~/.claude/{type}/
104
+ * - Copies new items to cc-tool/configs/{type}/
105
+ * - Registers them with enabled: true
106
+ * - Syncs back to Claude (since they were already there)
107
+ * Returns { success: true, imported: number, skipped: number, items: [...] }
108
+ */
109
+ router.post('/:type/import', async (req, res) => {
110
+ try {
111
+ const { type } = req.params;
112
+
113
+ const typeError = validateType(type);
114
+ if (typeError) {
115
+ return res.status(400).json({
116
+ success: false,
117
+ message: typeError
118
+ });
119
+ }
120
+
121
+ // Import from Claude Code directories
122
+ const result = registryService.importFromClaude(type);
123
+
124
+ // Sync imported items back to Claude (they were already there but now managed by cc-tool)
125
+ // This ensures consistency between registry and actual files
126
+ if (result.imported > 0) {
127
+ for (const name of result.items) {
128
+ const item = registryService.getItem(type, name);
129
+ if (item && item.enabled && item.platforms?.claude) {
130
+ syncManager.syncToClaude(type, name);
131
+ }
132
+ }
133
+ }
134
+
135
+ res.json({
136
+ success: true,
137
+ type,
138
+ imported: result.imported,
139
+ skipped: result.skipped,
140
+ items: result.items
141
+ });
142
+ } catch (err) {
143
+ console.error('[ConfigRegistry API] Import error:', err);
144
+ res.status(500).json({
145
+ success: false,
146
+ message: err.message
147
+ });
148
+ }
149
+ });
150
+
151
+ /**
152
+ * PUT /api/config-registry/:type/:name/toggle
153
+ * Toggle enabled/disabled status
154
+ * Body: { enabled: boolean }
155
+ * - Updates registry
156
+ * - If enabling: sync to platforms where platform=true
157
+ * - If disabling: remove from all platforms
158
+ * Returns { success: true, item: updatedEntry }
159
+ */
160
+ router.put('/:type/:name/toggle', async (req, res) => {
161
+ try {
162
+ const { type } = req.params;
163
+ const name = decodeURIComponent(req.params.name);
164
+ const { enabled } = req.body;
165
+
166
+ const typeError = validateType(type);
167
+ if (typeError) {
168
+ return res.status(400).json({
169
+ success: false,
170
+ message: typeError
171
+ });
172
+ }
173
+
174
+ if (typeof enabled !== 'boolean') {
175
+ return res.status(400).json({
176
+ success: false,
177
+ message: 'enabled must be a boolean'
178
+ });
179
+ }
180
+
181
+ // Check if item exists
182
+ const existing = registryService.getItem(type, name);
183
+ if (!existing) {
184
+ return res.status(404).json({
185
+ success: false,
186
+ message: `Item "${name}" not found in ${type}`
187
+ });
188
+ }
189
+
190
+ // Update registry
191
+ const item = registryService.toggleEnabled(type, name, enabled);
192
+
193
+ // Apply sync based on new state
194
+ if (enabled) {
195
+ // Sync to platforms where platform=true
196
+ if (item.platforms?.claude) {
197
+ syncManager.syncToClaude(type, name);
198
+ }
199
+ if (item.platforms?.codex) {
200
+ syncManager.syncToCodex(type, name);
201
+ }
202
+ } else {
203
+ // Remove from all platforms
204
+ syncManager.removeFromClaude(type, name);
205
+ syncManager.removeFromCodex(type, name);
206
+ }
207
+
208
+ res.json({
209
+ success: true,
210
+ item
211
+ });
212
+ } catch (err) {
213
+ console.error('[ConfigRegistry API] Toggle enabled error:', err);
214
+ res.status(500).json({
215
+ success: false,
216
+ message: err.message
217
+ });
218
+ }
219
+ });
220
+
221
+ /**
222
+ * PUT /api/config-registry/:type/:name/platform/:platform
223
+ * Toggle platform (claude/codex) for an item
224
+ * Body: { enabled: boolean }
225
+ * - Updates registry
226
+ * - If enabling platform: sync to that platform (if item is enabled)
227
+ * - If disabling platform: remove from that platform
228
+ * Returns { success: true, item: updatedEntry }
229
+ */
230
+ router.put('/:type/:name/platform/:platform', async (req, res) => {
231
+ try {
232
+ const { type, platform } = req.params;
233
+ const name = decodeURIComponent(req.params.name);
234
+ const { enabled } = req.body;
235
+
236
+ const typeError = validateType(type);
237
+ if (typeError) {
238
+ return res.status(400).json({
239
+ success: false,
240
+ message: typeError
241
+ });
242
+ }
243
+
244
+ const platformError = validatePlatform(platform);
245
+ if (platformError) {
246
+ return res.status(400).json({
247
+ success: false,
248
+ message: platformError
249
+ });
250
+ }
251
+
252
+ if (typeof enabled !== 'boolean') {
253
+ return res.status(400).json({
254
+ success: false,
255
+ message: 'enabled must be a boolean'
256
+ });
257
+ }
258
+
259
+ // Check if item exists
260
+ const existing = registryService.getItem(type, name);
261
+ if (!existing) {
262
+ return res.status(404).json({
263
+ success: false,
264
+ message: `Item "${name}" not found in ${type}`
265
+ });
266
+ }
267
+
268
+ // Update registry
269
+ const item = registryService.togglePlatform(type, name, platform, enabled);
270
+
271
+ // Apply sync based on new state
272
+ if (enabled && item.enabled) {
273
+ // Sync to this platform (only if item is enabled)
274
+ if (platform === 'claude') {
275
+ syncManager.syncToClaude(type, name);
276
+ } else if (platform === 'codex') {
277
+ syncManager.syncToCodex(type, name);
278
+ }
279
+ } else {
280
+ // Remove from this platform
281
+ if (platform === 'claude') {
282
+ syncManager.removeFromClaude(type, name);
283
+ } else if (platform === 'codex') {
284
+ syncManager.removeFromCodex(type, name);
285
+ }
286
+ }
287
+
288
+ res.json({
289
+ success: true,
290
+ item
291
+ });
292
+ } catch (err) {
293
+ console.error('[ConfigRegistry API] Toggle platform error:', err);
294
+ res.status(500).json({
295
+ success: false,
296
+ message: err.message
297
+ });
298
+ }
299
+ });
300
+
301
+ /**
302
+ * POST /api/config-registry/:type/sync
303
+ * Force sync all items of a type to their platforms based on registry
304
+ * Returns { success: true, synced: number }
305
+ */
306
+ router.post('/:type/sync', async (req, res) => {
307
+ try {
308
+ const { type } = req.params;
309
+
310
+ const typeError = validateType(type);
311
+ if (typeError) {
312
+ return res.status(400).json({
313
+ success: false,
314
+ message: typeError
315
+ });
316
+ }
317
+
318
+ // Get all items for this type
319
+ const items = registryService.listItems(type);
320
+
321
+ // Sync all items based on their registry state
322
+ const result = syncManager.syncAll(type, items);
323
+
324
+ res.json({
325
+ success: true,
326
+ type,
327
+ synced: result.synced.length,
328
+ removed: result.removed.length,
329
+ errors: result.errors,
330
+ warnings: result.warnings
331
+ });
332
+ } catch (err) {
333
+ console.error('[ConfigRegistry API] Sync all error:', err);
334
+ res.status(500).json({
335
+ success: false,
336
+ message: err.message
337
+ });
338
+ }
339
+ });
340
+
341
+ module.exports = router;
@@ -13,6 +13,8 @@ const { getChannelHealthStatus, resetChannelHealth } = require('../services/chan
13
13
  const { broadcastSchedulerState } = require('../websocket-server');
14
14
  const { isGeminiInstalled } = require('../services/gemini-config');
15
15
  const { testChannelSpeed, testMultipleChannels, getLatencyLevel } = require('../services/speed-test');
16
+ const { clearGeminiRedirectCache } = require('../gemini-proxy-server');
17
+ const { fetchModelsFromProvider } = require('../services/model-detector');
16
18
 
17
19
  module.exports = (config) => {
18
20
  /**
@@ -41,6 +43,42 @@ module.exports = (config) => {
41
43
  }
42
44
  });
43
45
 
46
+ /**
47
+ * GET /api/gemini/channels/:id/models
48
+ * 获取渠道可用模型列表
49
+ */
50
+ router.get('/:id/models', async (req, res) => {
51
+ try {
52
+ const { id } = req.params;
53
+ const channels = getChannels().channels || [];
54
+ const channel = channels.find(ch => ch.id === id);
55
+
56
+ if (!channel) {
57
+ return res.status(404).json({ error: '渠道不存在' });
58
+ }
59
+
60
+ // Gemini 渠道尝试作为 OpenAI 兼容获取,失败则回退
61
+ const result = await fetchModelsFromProvider(channel, 'openai_compatible');
62
+
63
+ res.json({
64
+ channelId: id,
65
+ models: result.models,
66
+ supported: result.supported,
67
+ cached: result.cached,
68
+ fallbackUsed: result.fallbackUsed,
69
+ fetchedAt: result.lastChecked || new Date().toISOString(),
70
+ error: result.error,
71
+ errorHint: result.errorHint
72
+ });
73
+ } catch (error) {
74
+ console.error('[Gemini Channels API] Error fetching models:', error);
75
+ res.status(500).json({
76
+ error: '获取模型列表失败',
77
+ channelId: req.params.id
78
+ });
79
+ }
80
+ });
81
+
44
82
  /**
45
83
  * POST /api/gemini/channels
46
84
  * 创建新渠道
@@ -86,6 +124,8 @@ module.exports = (config) => {
86
124
  const updates = req.body;
87
125
 
88
126
  const channel = updateChannel(channelId, updates);
127
+ // 清除该渠道的模型重定向日志缓存,使下次请求时重新打印
128
+ clearGeminiRedirectCache(channelId);
89
129
  res.json(channel);
90
130
  broadcastSchedulerState('gemini', getSchedulerState('gemini'));
91
131
  } catch (err) {
@@ -30,16 +30,16 @@ const permissionTemplatesService = require('../services/permission-templates-ser
30
30
  const router = express.Router();
31
31
 
32
32
  // Claude Code 设置文件路径
33
- function getClaudeSettingsPath(projectPath, isLocal = false) {
33
+ function getClaudeSettingsPath(projectPath) {
34
34
  if (projectPath) {
35
- return path.join(projectPath, '.claude', isLocal ? 'settings.local.json' : 'settings.json');
35
+ return path.join(projectPath, '.claude', 'settings.json');
36
36
  }
37
37
  return path.join(os.homedir(), '.claude', 'settings.json');
38
38
  }
39
39
 
40
40
  // 读取 Claude Code settings.json
41
- function readClaudeSettings(projectPath, isLocal = false) {
42
- const settingsPath = getClaudeSettingsPath(projectPath, isLocal);
41
+ function readClaudeSettings(projectPath) {
42
+ const settingsPath = getClaudeSettingsPath(projectPath);
43
43
  try {
44
44
  if (fs.existsSync(settingsPath)) {
45
45
  const content = fs.readFileSync(settingsPath, 'utf-8');
@@ -52,15 +52,29 @@ function readClaudeSettings(projectPath, isLocal = false) {
52
52
  }
53
53
 
54
54
  // 保存 Claude Code settings.json
55
- function saveClaudeSettings(projectPath, settings, isLocal = false) {
56
- const settingsPath = getClaudeSettingsPath(projectPath, isLocal);
55
+ function saveClaudeSettings(projectPath, settings) {
56
+ const settingsPath = getClaudeSettingsPath(projectPath);
57
57
  const settingsDir = path.dirname(settingsPath);
58
58
 
59
- if (!fs.existsSync(settingsDir)) {
60
- fs.mkdirSync(settingsDir, { recursive: true });
61
- }
59
+ try {
60
+ // 确保目录存在
61
+ if (!fs.existsSync(settingsDir)) {
62
+ fs.mkdirSync(settingsDir, { recursive: true });
63
+ }
62
64
 
63
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
65
+ // 写入文件
66
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
67
+
68
+ // 验证文件已创建
69
+ if (!fs.existsSync(settingsPath)) {
70
+ throw new Error('文件写入后验证失败,文件未被创建');
71
+ }
72
+
73
+ return { success: true, path: settingsPath };
74
+ } catch (err) {
75
+ console.error('[Permissions API] Error saving Claude settings:', err);
76
+ throw new Error(`保存配置文件失败: ${err.message}`);
77
+ }
64
78
  }
65
79
 
66
80
  // 全局 all-allow 状态(内存中)
@@ -117,11 +131,11 @@ router.get('/', (req, res) => {
117
131
  /**
118
132
  * 保存项目的命令执行权限设置
119
133
  * POST /api/permissions
120
- * Body: { projectPath, settings: { allow, deny }, isLocal }
134
+ * Body: { projectPath, settings: { allow, deny } }
121
135
  */
122
136
  router.post('/', (req, res) => {
123
137
  try {
124
- const { projectPath, settings: newPermissions, isLocal = false } = req.body;
138
+ const { projectPath, settings: newPermissions } = req.body;
125
139
 
126
140
  if (!projectPath) {
127
141
  return res.status(400).json({
@@ -138,7 +152,7 @@ router.post('/', (req, res) => {
138
152
  }
139
153
 
140
154
  // 读取现有设置
141
- const settings = readClaudeSettings(projectPath, isLocal);
155
+ const settings = readClaudeSettings(projectPath);
142
156
 
143
157
  // 更新权限设置(使用 Claude Code 的标准格式)
144
158
  settings.permissions = {
@@ -147,12 +161,13 @@ router.post('/', (req, res) => {
147
161
  };
148
162
 
149
163
  // 保存设置
150
- saveClaudeSettings(projectPath, settings, isLocal);
164
+ const saveResult = saveClaudeSettings(projectPath, settings);
151
165
 
152
166
  res.json({
153
167
  success: true,
154
168
  message: '权限设置已保存',
155
- savedTo: isLocal ? '.claude/settings.local.json' : '.claude/settings.json'
169
+ savedTo: '.claude/settings.json',
170
+ fullPath: saveResult.path
156
171
  });
157
172
  } catch (err) {
158
173
  console.error('[Permissions API] Save permissions error:', err);
@@ -20,6 +20,10 @@ let currentPort = null;
20
20
  // 用于存储每个请求的元数据
21
21
  const requestMetadata = new Map();
22
22
 
23
+ // 用于缓存已打印过的模型重定向规则,避免重复打印
24
+ // 格式: { channelId: { "originalModel": "redirectedModel", ... } }
25
+ const printedRedirectCache = new Map();
26
+
23
27
  // OpenAI 模型定价(每百万 tokens 的价格,单位:美元)
24
28
  // Claude 模型使用 config/model-pricing.js 中的集中定价
25
29
  const PRICING = {
@@ -283,7 +287,14 @@ async function startCodexProxyServer(options = {}) {
283
287
  req.body.model = redirectedModel;
284
288
  // 更新 rawBody 以匹配修改后的 body
285
289
  req.rawBody = Buffer.from(JSON.stringify(req.body));
286
- console.log(`[Codex Model Redirect] ${originalModel} → ${redirectedModel} (channel: ${channel.name})`);
290
+
291
+ // 只在重定向规则变化时打印日志(避免每次请求都打印)
292
+ const cachedRedirects = printedRedirectCache.get(channel.id) || {};
293
+ if (cachedRedirects[originalModel] !== redirectedModel) {
294
+ cachedRedirects[originalModel] = redirectedModel;
295
+ printedRedirectCache.set(channel.id, cachedRedirects);
296
+ console.log(`[Codex Model Redirect] ${originalModel} → ${redirectedModel} (channel: ${channel.name})`);
297
+ }
287
298
  }
288
299
  }
289
300
 
@@ -648,8 +659,22 @@ function getCodexProxyStatus() {
648
659
  };
649
660
  }
650
661
 
662
+ /**
663
+ * 清除指定渠道的模型重定向日志缓存
664
+ * 用于在渠道配置更新后触发重新打印日志
665
+ * @param {string} channelId - 渠道 ID
666
+ */
667
+ function clearCodexRedirectCache(channelId) {
668
+ if (channelId) {
669
+ printedRedirectCache.delete(channelId);
670
+ } else {
671
+ printedRedirectCache.clear();
672
+ }
673
+ }
674
+
651
675
  module.exports = {
652
676
  startCodexProxyServer,
653
677
  stopCodexProxyServer,
654
- getCodexProxyStatus
678
+ getCodexProxyStatus,
679
+ clearCodexRedirectCache
655
680
  };