@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,386 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ /**
6
+ * 统计服务 - 数据采集和存储
7
+ *
8
+ * 文件结构:
9
+ * ~/.claude/cc-tool/
10
+ * ├── statistics.json # 总体统计(实时更新)
11
+ * ├── daily-stats/
12
+ * │ ├── 2025-11-22.json # 每日汇总统计
13
+ * │ └── 2025-11-23.json
14
+ * └── request-logs/
15
+ * ├── 2025-11/
16
+ * │ ├── 22.jsonl # 每日详细日志(JSONL格式)
17
+ * │ └── 23.jsonl
18
+ * └── 2025-12/
19
+ */
20
+
21
+ // 获取基础目录
22
+ function getBaseDir() {
23
+ const dir = path.join(os.homedir(), '.claude', 'cc-tool');
24
+ if (!fs.existsSync(dir)) {
25
+ fs.mkdirSync(dir, { recursive: true });
26
+ }
27
+ return dir;
28
+ }
29
+
30
+ // 获取每日统计目录
31
+ function getDailyStatsDir() {
32
+ const dir = path.join(getBaseDir(), 'daily-stats');
33
+ if (!fs.existsSync(dir)) {
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ }
36
+ return dir;
37
+ }
38
+
39
+ // 获取请求日志目录
40
+ function getRequestLogsDir(year, month) {
41
+ const baseDir = path.join(getBaseDir(), 'request-logs', `${year}-${month.toString().padStart(2, '0')}`);
42
+ if (!fs.existsSync(baseDir)) {
43
+ fs.mkdirSync(baseDir, { recursive: true });
44
+ }
45
+ return baseDir;
46
+ }
47
+
48
+ // 获取统计文件路径
49
+ function getStatisticsFilePath() {
50
+ return path.join(getBaseDir(), 'statistics.json');
51
+ }
52
+
53
+ // 获取每日统计文件路径
54
+ function getDailyStatsFilePath(date) {
55
+ // date 格式: YYYY-MM-DD
56
+ return path.join(getDailyStatsDir(), `${date}.json`);
57
+ }
58
+
59
+ // 获取请求日志文件路径
60
+ function getRequestLogFilePath(year, month, day) {
61
+ const dir = getRequestLogsDir(year, month);
62
+ return path.join(dir, `${day.toString().padStart(2, '0')}.jsonl`);
63
+ }
64
+
65
+ // 加载总体统计
66
+ function loadStatistics() {
67
+ const filePath = getStatisticsFilePath();
68
+ try {
69
+ if (fs.existsSync(filePath)) {
70
+ const data = fs.readFileSync(filePath, 'utf8');
71
+ return JSON.parse(data);
72
+ }
73
+ } catch (err) {
74
+ console.error('Failed to load statistics:', err);
75
+ }
76
+
77
+ // 返回默认结构
78
+ return {
79
+ version: '2.0',
80
+ lastUpdated: new Date().toISOString(),
81
+ global: {
82
+ totalRequests: 0,
83
+ totalTokens: 0,
84
+ totalCost: 0
85
+ },
86
+ byToolType: {},
87
+ byChannel: {},
88
+ byModel: {}
89
+ };
90
+ }
91
+
92
+ // 保存总体统计
93
+ function saveStatistics(stats) {
94
+ const filePath = getStatisticsFilePath();
95
+ stats.lastUpdated = new Date().toISOString();
96
+
97
+ try {
98
+ fs.writeFileSync(filePath, JSON.stringify(stats, null, 2), 'utf8');
99
+ } catch (err) {
100
+ console.error('Failed to save statistics:', err);
101
+ }
102
+ }
103
+
104
+ // 加载每日统计
105
+ function loadDailyStats(date) {
106
+ const filePath = getDailyStatsFilePath(date);
107
+ try {
108
+ if (fs.existsSync(filePath)) {
109
+ const data = fs.readFileSync(filePath, 'utf8');
110
+ return JSON.parse(data);
111
+ }
112
+ } catch (err) {
113
+ console.error('Failed to load daily stats:', err);
114
+ }
115
+
116
+ // 返回默认结构
117
+ return {
118
+ date: date,
119
+ summary: {
120
+ requests: 0,
121
+ tokens: 0,
122
+ cost: 0
123
+ },
124
+ hourly: {}, // 按小时统计
125
+ byToolType: {},
126
+ byChannel: {},
127
+ byModel: {}
128
+ };
129
+ }
130
+
131
+ // 保存每日统计
132
+ function saveDailyStats(date, stats) {
133
+ const filePath = getDailyStatsFilePath(date);
134
+
135
+ try {
136
+ fs.writeFileSync(filePath, JSON.stringify(stats, null, 2), 'utf8');
137
+ } catch (err) {
138
+ console.error('Failed to save daily stats:', err);
139
+ }
140
+ }
141
+
142
+ // 追加请求日志(JSONL格式)
143
+ function appendRequestLog(logEntry) {
144
+ const timestamp = new Date(logEntry.timestamp);
145
+ const year = timestamp.getFullYear();
146
+ const month = timestamp.getMonth() + 1;
147
+ const day = timestamp.getDate();
148
+
149
+ const filePath = getRequestLogFilePath(year, month, day);
150
+
151
+ try {
152
+ // JSONL 格式:每行一个 JSON 对象
153
+ const line = JSON.stringify(logEntry) + '\n';
154
+ fs.appendFileSync(filePath, line, 'utf8');
155
+ } catch (err) {
156
+ console.error('Failed to append request log:', err);
157
+ }
158
+ }
159
+
160
+ // 初始化统计对象
161
+ function initStatsObject() {
162
+ return {
163
+ requests: 0,
164
+ tokens: {
165
+ input: 0,
166
+ output: 0,
167
+ cacheCreation: 0,
168
+ cacheRead: 0,
169
+ total: 0
170
+ },
171
+ cost: 0
172
+ };
173
+ }
174
+
175
+ // 更新统计数据
176
+ function updateStats(stats, tokens, cost) {
177
+ stats.requests += 1;
178
+ stats.tokens.input += tokens.input || 0;
179
+ stats.tokens.output += tokens.output || 0;
180
+ stats.tokens.cacheCreation += tokens.cacheCreation || 0;
181
+ stats.tokens.cacheRead += tokens.cacheRead || 0;
182
+ stats.tokens.total += tokens.total || 0;
183
+ stats.cost += cost || 0;
184
+ }
185
+
186
+ /**
187
+ * 记录一次请求
188
+ * @param {Object} requestData - 请求数据
189
+ */
190
+ function recordRequest(requestData) {
191
+ try {
192
+ const {
193
+ id,
194
+ timestamp,
195
+ toolType = 'claude-code',
196
+ channel,
197
+ channelId,
198
+ model,
199
+ tokens,
200
+ duration,
201
+ success,
202
+ cost = 0,
203
+ session,
204
+ project
205
+ } = requestData;
206
+
207
+ // 1. 写入详细日志
208
+ const logEntry = {
209
+ id,
210
+ timestamp,
211
+ toolType,
212
+ channel,
213
+ channelId,
214
+ model,
215
+ tokens,
216
+ duration,
217
+ success,
218
+ cost,
219
+ session,
220
+ project
221
+ };
222
+ appendRequestLog(logEntry);
223
+
224
+ // 2. 更新总体统计
225
+ const globalStats = loadStatistics();
226
+
227
+ // 更新全局统计
228
+ globalStats.global.totalRequests += 1;
229
+ globalStats.global.totalTokens += tokens.total || 0;
230
+ globalStats.global.totalCost += cost || 0;
231
+
232
+ // 按工具类型统计
233
+ if (!globalStats.byToolType[toolType]) {
234
+ globalStats.byToolType[toolType] = {
235
+ ...initStatsObject(),
236
+ channels: {},
237
+ models: {}
238
+ };
239
+ }
240
+ updateStats(globalStats.byToolType[toolType], tokens, cost);
241
+
242
+ // 按工具类型 -> 渠道统计
243
+ if (!globalStats.byToolType[toolType].channels[channelId]) {
244
+ globalStats.byToolType[toolType].channels[channelId] = {
245
+ name: channel,
246
+ ...initStatsObject(),
247
+ firstUsed: timestamp,
248
+ lastUsed: timestamp
249
+ };
250
+ } else {
251
+ globalStats.byToolType[toolType].channels[channelId].lastUsed = timestamp;
252
+ }
253
+ updateStats(globalStats.byToolType[toolType].channels[channelId], tokens, cost);
254
+
255
+ // 按工具类型 -> 模型统计
256
+ if (!globalStats.byToolType[toolType].models[model]) {
257
+ globalStats.byToolType[toolType].models[model] = initStatsObject();
258
+ }
259
+ updateStats(globalStats.byToolType[toolType].models[model], tokens, cost);
260
+
261
+ // 按渠道统计(跨工具)
262
+ if (!globalStats.byChannel[channelId]) {
263
+ globalStats.byChannel[channelId] = {
264
+ toolType,
265
+ name: channel,
266
+ ...initStatsObject(),
267
+ firstUsed: timestamp,
268
+ lastUsed: timestamp
269
+ };
270
+ } else {
271
+ globalStats.byChannel[channelId].lastUsed = timestamp;
272
+ }
273
+ updateStats(globalStats.byChannel[channelId], tokens, cost);
274
+
275
+ // 按模型统计(跨工具)
276
+ if (!globalStats.byModel[model]) {
277
+ globalStats.byModel[model] = {
278
+ toolType,
279
+ ...initStatsObject()
280
+ };
281
+ }
282
+ updateStats(globalStats.byModel[model], tokens, cost);
283
+
284
+ saveStatistics(globalStats);
285
+
286
+ // 3. 更新每日统计
287
+ const date = new Date(timestamp).toISOString().split('T')[0]; // YYYY-MM-DD
288
+ const hour = new Date(timestamp).getHours().toString().padStart(2, '0'); // HH
289
+
290
+ const dailyStats = loadDailyStats(date);
291
+
292
+ // 更新每日汇总
293
+ dailyStats.summary.requests += 1;
294
+ dailyStats.summary.tokens += tokens.total || 0;
295
+ dailyStats.summary.cost += cost || 0;
296
+
297
+ // 按小时统计
298
+ if (!dailyStats.hourly[hour]) {
299
+ dailyStats.hourly[hour] = {
300
+ ...initStatsObject(),
301
+ byToolType: {}
302
+ };
303
+ }
304
+ updateStats(dailyStats.hourly[hour], tokens, cost);
305
+
306
+ // 按小时 -> 工具类型
307
+ if (!dailyStats.hourly[hour].byToolType[toolType]) {
308
+ dailyStats.hourly[hour].byToolType[toolType] = initStatsObject();
309
+ }
310
+ updateStats(dailyStats.hourly[hour].byToolType[toolType], tokens, cost);
311
+
312
+ // 按工具类型统计
313
+ if (!dailyStats.byToolType[toolType]) {
314
+ dailyStats.byToolType[toolType] = {
315
+ ...initStatsObject(),
316
+ channels: {},
317
+ models: {}
318
+ };
319
+ }
320
+ updateStats(dailyStats.byToolType[toolType], tokens, cost);
321
+
322
+ // 按工具类型 -> 渠道
323
+ if (!dailyStats.byToolType[toolType].channels) {
324
+ dailyStats.byToolType[toolType].channels = {};
325
+ }
326
+ if (!dailyStats.byToolType[toolType].channels[channelId]) {
327
+ dailyStats.byToolType[toolType].channels[channelId] = {
328
+ name: channel,
329
+ ...initStatsObject()
330
+ };
331
+ }
332
+ updateStats(dailyStats.byToolType[toolType].channels[channelId], tokens, cost);
333
+
334
+ // 按渠道统计
335
+ if (!dailyStats.byChannel[channelId]) {
336
+ dailyStats.byChannel[channelId] = {
337
+ toolType,
338
+ name: channel,
339
+ ...initStatsObject()
340
+ };
341
+ }
342
+ updateStats(dailyStats.byChannel[channelId], tokens, cost);
343
+
344
+ // 按模型统计
345
+ if (!dailyStats.byModel[model]) {
346
+ dailyStats.byModel[model] = {
347
+ toolType,
348
+ ...initStatsObject()
349
+ };
350
+ }
351
+ updateStats(dailyStats.byModel[model], tokens, cost);
352
+
353
+ saveDailyStats(date, dailyStats);
354
+ } catch (err) {
355
+ console.error('[Statistics] Failed to record request:', err);
356
+ }
357
+ }
358
+
359
+ /**
360
+ * 获取统计数据
361
+ */
362
+ function getStatistics() {
363
+ return loadStatistics();
364
+ }
365
+
366
+ /**
367
+ * 获取每日统计
368
+ */
369
+ function getDailyStatistics(date) {
370
+ return loadDailyStats(date);
371
+ }
372
+
373
+ /**
374
+ * 获取今日统计
375
+ */
376
+ function getTodayStatistics() {
377
+ const today = new Date().toISOString().split('T')[0];
378
+ return loadDailyStats(today);
379
+ }
380
+
381
+ module.exports = {
382
+ recordRequest,
383
+ getStatistics,
384
+ getDailyStatistics,
385
+ getTodayStatistics
386
+ };
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Terminal Commands Service - CLI 启动命令配置管理
3
+ * 管理不同渠道 (Claude/Codex/Gemini) 的 CLI 启动命令
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ // 配置文件路径
11
+ const CONFIG_DIR = path.join(os.homedir(), '.claude', 'cc-tool');
12
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'terminal-commands.json');
13
+
14
+ // 默认命令配置
15
+ const DEFAULT_COMMANDS = {
16
+ claude: {
17
+ name: 'Claude Code',
18
+ newSession: 'claude',
19
+ resumeSession: 'claude -r {sessionId}',
20
+ description: 'Anthropic Claude Code CLI'
21
+ },
22
+ codex: {
23
+ name: 'Codex CLI',
24
+ newSession: 'codex',
25
+ resumeSession: 'codex resume {sessionId}',
26
+ description: 'OpenAI Codex CLI'
27
+ },
28
+ gemini: {
29
+ name: 'Gemini CLI',
30
+ newSession: 'gemini',
31
+ resumeSession: 'gemini -r {sessionId}',
32
+ description: 'Google Gemini CLI'
33
+ }
34
+ };
35
+
36
+ /**
37
+ * 确保配置目录存在
38
+ */
39
+ function ensureConfigDir() {
40
+ if (!fs.existsSync(CONFIG_DIR)) {
41
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
42
+ }
43
+ }
44
+
45
+ /**
46
+ * 加载命令配置
47
+ * @returns {Object} 命令配置
48
+ */
49
+ function loadTerminalCommands() {
50
+ try {
51
+ ensureConfigDir();
52
+
53
+ if (fs.existsSync(CONFIG_FILE)) {
54
+ const content = fs.readFileSync(CONFIG_FILE, 'utf8');
55
+ const saved = JSON.parse(content);
56
+
57
+ // 合并默认配置和已保存配置
58
+ return {
59
+ claude: { ...DEFAULT_COMMANDS.claude, ...saved.claude },
60
+ codex: { ...DEFAULT_COMMANDS.codex, ...saved.codex },
61
+ gemini: { ...DEFAULT_COMMANDS.gemini, ...saved.gemini }
62
+ };
63
+ }
64
+ } catch (err) {
65
+ console.error('Failed to load terminal commands config:', err.message);
66
+ }
67
+
68
+ return { ...DEFAULT_COMMANDS };
69
+ }
70
+
71
+ /**
72
+ * 保存命令配置
73
+ * @param {Object} commands - 命令配置
74
+ * @returns {boolean} 是否成功
75
+ */
76
+ function saveTerminalCommands(commands) {
77
+ try {
78
+ ensureConfigDir();
79
+
80
+ // 验证配置格式
81
+ const validated = {};
82
+ for (const channel of ['claude', 'codex', 'gemini']) {
83
+ if (commands[channel]) {
84
+ validated[channel] = {
85
+ name: commands[channel].name || DEFAULT_COMMANDS[channel].name,
86
+ newSession: commands[channel].newSession || DEFAULT_COMMANDS[channel].newSession,
87
+ resumeSession: commands[channel].resumeSession || DEFAULT_COMMANDS[channel].resumeSession,
88
+ description: commands[channel].description || DEFAULT_COMMANDS[channel].description
89
+ };
90
+ } else {
91
+ validated[channel] = { ...DEFAULT_COMMANDS[channel] };
92
+ }
93
+ }
94
+
95
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(validated, null, 2), 'utf8');
96
+ return true;
97
+ } catch (err) {
98
+ console.error('Failed to save terminal commands config:', err.message);
99
+ return false;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * 获取指定渠道的启动命令
105
+ * @param {string} channel - 渠道类型 (claude/codex/gemini)
106
+ * @param {string|null} sessionId - 会话 ID (可选)
107
+ * @param {string|null} cwd - 工作目录 (可选)
108
+ * @returns {string} 启动命令
109
+ */
110
+ function getCommandForChannel(channel, sessionId = null, cwd = null) {
111
+ const commands = loadTerminalCommands();
112
+ const channelConfig = commands[channel] || commands.claude;
113
+
114
+ let command;
115
+ if (sessionId) {
116
+ // 恢复会话命令
117
+ command = channelConfig.resumeSession;
118
+ command = command.replace(/{sessionId}/g, sessionId);
119
+ } else {
120
+ // 新会话命令
121
+ command = channelConfig.newSession;
122
+ }
123
+
124
+ // 替换工作目录占位符
125
+ if (cwd) {
126
+ command = command.replace(/{cwd}/g, cwd);
127
+ }
128
+
129
+ return command;
130
+ }
131
+
132
+ /**
133
+ * 获取默认命令配置
134
+ * @returns {Object} 默认配置
135
+ */
136
+ function getDefaultCommands() {
137
+ return { ...DEFAULT_COMMANDS };
138
+ }
139
+
140
+ /**
141
+ * 重置为默认配置
142
+ * @returns {boolean} 是否成功
143
+ */
144
+ function resetToDefaults() {
145
+ return saveTerminalCommands(DEFAULT_COMMANDS);
146
+ }
147
+
148
+ module.exports = {
149
+ loadTerminalCommands,
150
+ saveTerminalCommands,
151
+ getCommandForChannel,
152
+ getDefaultCommands,
153
+ resetToDefaults,
154
+ DEFAULT_COMMANDS
155
+ };
@@ -0,0 +1,140 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { detectAvailableTerminals, getDefaultTerminal } = require('./terminal-detector');
5
+
6
+ /**
7
+ * 获取配置文件路径
8
+ */
9
+ function getConfigFilePath() {
10
+ const ccToolDir = path.join(os.homedir(), '.claude', 'cc-tool');
11
+ if (!fs.existsSync(ccToolDir)) {
12
+ fs.mkdirSync(ccToolDir, { recursive: true });
13
+ }
14
+ return path.join(ccToolDir, 'terminal-config.json');
15
+ }
16
+
17
+ /**
18
+ * 加载终端配置
19
+ */
20
+ function loadTerminalConfig() {
21
+ const configPath = getConfigFilePath();
22
+
23
+ try {
24
+ if (fs.existsSync(configPath)) {
25
+ const data = fs.readFileSync(configPath, 'utf8');
26
+ return JSON.parse(data);
27
+ }
28
+ } catch (err) {
29
+ console.error('Failed to load terminal config:', err);
30
+ }
31
+
32
+ // 返回默认配置
33
+ const defaultTerminal = getDefaultTerminal();
34
+ return {
35
+ selectedTerminal: defaultTerminal ? defaultTerminal.id : null,
36
+ customCommand: null
37
+ };
38
+ }
39
+
40
+ /**
41
+ * 保存终端配置
42
+ */
43
+ function saveTerminalConfig(config) {
44
+ const configPath = getConfigFilePath();
45
+
46
+ try {
47
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
48
+ return { success: true };
49
+ } catch (err) {
50
+ console.error('Failed to save terminal config:', err);
51
+ throw new Error('Failed to save terminal config: ' + err.message);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 获取当前选中的终端配置
57
+ */
58
+ function getSelectedTerminal() {
59
+ const config = loadTerminalConfig();
60
+ const availableTerminals = detectAvailableTerminals();
61
+
62
+ // 如果配置了自定义命令,返回自定义配置
63
+ if (config.customCommand) {
64
+ return {
65
+ id: 'custom',
66
+ name: 'Custom',
67
+ available: true,
68
+ isDefault: false,
69
+ command: config.customCommand
70
+ };
71
+ }
72
+
73
+ // 查找选中的终端
74
+ const selectedTerminal = availableTerminals.find(t => t.id === config.selectedTerminal);
75
+
76
+ // 如果找到则返回,否则返回默认终端
77
+ return selectedTerminal || getDefaultTerminal();
78
+ }
79
+
80
+ /**
81
+ * 获取终端启动命令(填充参数后)
82
+ * @param {string} cwd - 工作目录
83
+ * @param {string} sessionId - 会话ID(用于 Claude -r 参数)
84
+ * @param {string} toolType - 工具类型 ('claude', 'codex', 'gemini')
85
+ * @param {string} customCliCommand - 自定义 CLI 命令(如 "gemini --resume latest"),如果提供则替换默认的 claude 命令
86
+ */
87
+ function getTerminalLaunchCommand(cwd, sessionId, toolType, customCliCommand) {
88
+ const terminal = getSelectedTerminal();
89
+
90
+ if (!terminal) {
91
+ throw new Error('No terminal available');
92
+ }
93
+
94
+ let command = terminal.command;
95
+
96
+ // 根据工具类型构建 CLI 命令
97
+ let cliCommand;
98
+ if (customCliCommand) {
99
+ cliCommand = customCliCommand;
100
+ } else {
101
+ // 根据工具类型选择对应的 CLI 命令
102
+ switch (toolType) {
103
+ case 'codex':
104
+ cliCommand = `codex resume {sessionId}`;
105
+ break;
106
+ case 'gemini':
107
+ cliCommand = `gemini -r {sessionId}`;
108
+ break;
109
+ case 'claude':
110
+ default:
111
+ cliCommand = `claude -r {sessionId}`;
112
+ break;
113
+ }
114
+ }
115
+
116
+ // 替换 sessionId 占位符
117
+ cliCommand = cliCommand.replace(/{sessionId}/g, sessionId);
118
+
119
+ // 替换命令中的 claude 相关部分为实际的 CLI 命令
120
+ command = command
121
+ .replace(/claude\s+-r\s+\{sessionId\}/g, cliCommand)
122
+ .replace(/claude\s+-r\s+{sessionId}/g, cliCommand)
123
+ .replace(/claude -r {sessionId}/g, cliCommand);
124
+
125
+ // 替换 cwd 占位符
126
+ command = command.replace(/{cwd}/g, cwd);
127
+
128
+ return {
129
+ command,
130
+ terminalId: terminal.id,
131
+ terminalName: terminal.name
132
+ };
133
+ }
134
+
135
+ module.exports = {
136
+ loadTerminalConfig,
137
+ saveTerminalConfig,
138
+ getSelectedTerminal,
139
+ getTerminalLaunchCommand
140
+ };