@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,100 @@
1
+ // 交互提示
2
+ const inquirer = require('inquirer');
3
+ const chalk = require('chalk');
4
+
5
+ /**
6
+ * 选择会话提示
7
+ */
8
+ async function promptSelectSession(choices) {
9
+ const { sessionId } = await inquirer.prompt([
10
+ {
11
+ type: 'list',
12
+ name: 'sessionId',
13
+ message: '选择会话或操作:',
14
+ pageSize: 15,
15
+ choices: choices,
16
+ },
17
+ ]);
18
+
19
+ return sessionId;
20
+ }
21
+
22
+ /**
23
+ * Fork 确认提示
24
+ */
25
+ async function promptForkConfirm() {
26
+ const { action } = await inquirer.prompt([
27
+ {
28
+ type: 'list',
29
+ name: 'action',
30
+ message: chalk.bold('选择恢复方式:'),
31
+ default: 'continue',
32
+ choices: [
33
+ {
34
+ name: chalk.green('📝 继续原会话 (推荐) - 在原会话上继续对话,所有内容会追加到原文件'),
35
+ value: 'continue',
36
+ },
37
+ {
38
+ name: chalk.yellow('🌿 创建新分支 (Fork) - 基于原会话创建新会话,保留原会话不变'),
39
+ value: 'fork',
40
+ },
41
+ new inquirer.Separator(chalk.gray('─'.repeat(14))),
42
+ { name: chalk.blue('↩️ 返回重新选择'), value: 'back' },
43
+ ],
44
+ },
45
+ ]);
46
+
47
+ return action;
48
+ }
49
+
50
+ /**
51
+ * 搜索关键词提示
52
+ */
53
+ async function promptSearchKeyword() {
54
+ const { keyword } = await inquirer.prompt([
55
+ {
56
+ type: 'input',
57
+ name: 'keyword',
58
+ message: chalk.cyan('🔎 输入搜索关键词:'),
59
+ validate: (input) => {
60
+ if (!input.trim()) {
61
+ return '请输入搜索关键词';
62
+ }
63
+ return true;
64
+ },
65
+ },
66
+ ]);
67
+
68
+ return keyword.trim();
69
+ }
70
+
71
+ /**
72
+ * 选择项目提示
73
+ */
74
+ async function promptSelectProject(projects) {
75
+ // 添加取消选项
76
+ const choices = [
77
+ ...projects,
78
+ new inquirer.Separator(chalk.gray('─'.repeat(14))),
79
+ { name: chalk.gray('↩️ 取消切换'), value: null }
80
+ ];
81
+
82
+ const { project } = await inquirer.prompt([
83
+ {
84
+ type: 'list',
85
+ name: 'project',
86
+ message: chalk.cyan('选择项目:'),
87
+ pageSize: 15,
88
+ choices: choices,
89
+ },
90
+ ]);
91
+
92
+ return project;
93
+ }
94
+
95
+ module.exports = {
96
+ promptSelectSession,
97
+ promptForkConfirm,
98
+ promptSearchKeyword,
99
+ promptSelectProject,
100
+ };
@@ -0,0 +1,43 @@
1
+ // 格式化工具函数
2
+
3
+ /**
4
+ * 格式化时间
5
+ */
6
+ function formatTime(date) {
7
+ const now = new Date();
8
+ const diff = now - new Date(date);
9
+ const minutes = Math.floor(diff / 60000);
10
+ const hours = Math.floor(diff / 3600000);
11
+ const days = Math.floor(diff / 86400000);
12
+
13
+ if (minutes < 1) return '刚刚';
14
+ if (minutes < 60) return `${minutes}分钟前`;
15
+ if (hours < 24) return `${hours}小时前`;
16
+ if (days < 30) return `${days}天前`;
17
+ return new Date(date).toLocaleDateString('zh-CN');
18
+ }
19
+
20
+ /**
21
+ * 格式化文件大小
22
+ */
23
+ function formatSize(bytes) {
24
+ if (bytes < 1024) return `${bytes}B`;
25
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
26
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
27
+ }
28
+
29
+ /**
30
+ * 截断文本
31
+ */
32
+ function truncate(text, maxLength) {
33
+ if (!text) return '';
34
+ text = text.replace(/\s+/g, ' ').trim();
35
+ if (text.length <= maxLength) return text;
36
+ return text.substring(0, maxLength) + '...';
37
+ }
38
+
39
+ module.exports = {
40
+ formatTime,
41
+ formatSize,
42
+ truncate,
43
+ };
@@ -0,0 +1,94 @@
1
+ const { execSync } = require('child_process');
2
+ const net = require('net');
3
+
4
+ /**
5
+ * 检查端口是否被占用
6
+ */
7
+ function isPortInUse(port) {
8
+ return new Promise((resolve) => {
9
+ const server = net.createServer();
10
+
11
+ server.once('error', (err) => {
12
+ if (err.code === 'EADDRINUSE') {
13
+ resolve(true);
14
+ } else {
15
+ resolve(false);
16
+ }
17
+ });
18
+
19
+ server.once('listening', () => {
20
+ server.close();
21
+ resolve(false);
22
+ });
23
+
24
+ server.listen(port);
25
+ });
26
+ }
27
+
28
+ /**
29
+ * 查找占用端口的进程PID
30
+ */
31
+ function findProcessByPort(port) {
32
+ try {
33
+ // macOS/Linux 使用 lsof
34
+ const result = execSync(`lsof -ti :${port}`, { encoding: 'utf-8' }).trim();
35
+ return result.split('\n').filter(pid => pid);
36
+ } catch (err) {
37
+ // 如果 lsof 失败,尝试使用其他命令
38
+ try {
39
+ // 适用于某些 Linux 系统
40
+ const result = execSync(`fuser ${port}/tcp 2>/dev/null`, { encoding: 'utf-8' }).trim();
41
+ return result.split(/\s+/).filter(pid => pid);
42
+ } catch (e) {
43
+ return [];
44
+ }
45
+ }
46
+ }
47
+
48
+ /**
49
+ * 杀掉占用端口的进程
50
+ */
51
+ function killProcessByPort(port) {
52
+ try {
53
+ const pids = findProcessByPort(port);
54
+ if (pids.length === 0) {
55
+ return false;
56
+ }
57
+
58
+ pids.forEach(pid => {
59
+ try {
60
+ execSync(`kill -9 ${pid}`, { stdio: 'ignore' });
61
+ } catch (err) {
62
+ // 忽略单个进程杀掉失败的错误
63
+ }
64
+ });
65
+
66
+ return true;
67
+ } catch (err) {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * 等待端口释放
74
+ */
75
+ async function waitForPortRelease(port, timeout = 3000) {
76
+ const startTime = Date.now();
77
+
78
+ while (Date.now() - startTime < timeout) {
79
+ const inUse = await isPortInUse(port);
80
+ if (!inUse) {
81
+ return true;
82
+ }
83
+ await new Promise(resolve => setTimeout(resolve, 100));
84
+ }
85
+
86
+ return false;
87
+ }
88
+
89
+ module.exports = {
90
+ isPortInUse,
91
+ findProcessByPort,
92
+ killProcessByPort,
93
+ waitForPortRelease
94
+ };
@@ -0,0 +1,239 @@
1
+ // 会话相关工具函数
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const chalk = require('chalk');
5
+
6
+ /**
7
+ * 获取会话目录
8
+ */
9
+ function getSessionsDir(config) {
10
+ return path.join(config.projectsDir, config.currentProject);
11
+ }
12
+
13
+ /**
14
+ * 获取所有会话文件
15
+ */
16
+ function getAllSessions(config) {
17
+ const sessionsDir = getSessionsDir(config);
18
+ if (!fs.existsSync(sessionsDir)) {
19
+ console.log(chalk.red(`会话目录不存在: ${sessionsDir}`));
20
+ return [];
21
+ }
22
+
23
+ const files = fs.readdirSync(sessionsDir)
24
+ .filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'))
25
+ .map(file => {
26
+ const filePath = path.join(sessionsDir, file);
27
+ const stats = fs.statSync(filePath);
28
+ return {
29
+ sessionId: file.replace('.jsonl', ''),
30
+ filePath,
31
+ size: stats.size,
32
+ mtime: stats.mtime,
33
+ };
34
+ })
35
+ .sort((a, b) => b.mtime - a.mtime);
36
+
37
+ return files;
38
+ }
39
+
40
+ /**
41
+ * 快速解析会话信息(读取开头和结尾)
42
+ */
43
+ function parseSessionInfoFast(filePath) {
44
+ try {
45
+ const fileSize = fs.statSync(filePath).size;
46
+
47
+ // 如果文件太大(>10MB),只读取开头和结尾
48
+ if (fileSize > 10 * 1024 * 1024) {
49
+ const fd = fs.openSync(filePath, 'r');
50
+
51
+ // 读取开头 32KB(扩大范围以找到第一条消息)
52
+ const headBuffer = Buffer.alloc(32 * 1024);
53
+ const headBytesRead = fs.readSync(fd, headBuffer, 0, 32 * 1024, 0);
54
+
55
+ // 读取结尾 8KB
56
+ const tailBuffer = Buffer.alloc(8192);
57
+ const tailOffset = Math.max(0, fileSize - 8192);
58
+ fs.readSync(fd, tailBuffer, 0, 8192, tailOffset);
59
+
60
+ fs.closeSync(fd);
61
+
62
+ const headContent = headBuffer.slice(0, headBytesRead).toString('utf8');
63
+ const tailContent = tailBuffer.toString('utf8');
64
+
65
+ const headLines = headContent.split('\n');
66
+ const tailLines = tailContent.split('\n').slice(-20);
67
+
68
+ return parseLinesWithTail(headLines, tailLines);
69
+ }
70
+
71
+ // 小文件直接读取
72
+ const content = fs.readFileSync(filePath, 'utf8');
73
+ const lines = content.split('\n');
74
+
75
+ return parseLinesWithTail(lines, lines.slice(-20));
76
+ } catch (error) {
77
+ return { summary: '', gitBranch: '', firstMessage: '', lastMessage: '', messageCount: 0 };
78
+ }
79
+ }
80
+
81
+ /**
82
+ * 解析行数据(包含开头和结尾)
83
+ */
84
+ function parseLinesWithTail(headLines, tailLines) {
85
+ let summary = '';
86
+ let gitBranch = '';
87
+ let firstMessage = '';
88
+ let lastMessage = '';
89
+ let lastUserMessage = '';
90
+
91
+ // 解析开头(查找第一条有效消息,跳过 file-history-snapshot)
92
+ for (const line of headLines) {
93
+ if (!line.trim()) continue;
94
+
95
+ try {
96
+ const json = JSON.parse(line);
97
+
98
+ if (json.type === 'summary' && json.summary) {
99
+ summary = json.summary;
100
+ }
101
+
102
+ if (json.gitBranch && !gitBranch) {
103
+ gitBranch = json.gitBranch;
104
+ }
105
+
106
+ // 优先找用户消息作为首条消息
107
+ if (json.type === 'user' && json.message && json.message.content !== 'Warmup' && !firstMessage) {
108
+ firstMessage = typeof json.message.content === 'string'
109
+ ? json.message.content
110
+ : JSON.stringify(json.message.content);
111
+ }
112
+ } catch (e) {
113
+ // 忽略解析错误
114
+ }
115
+ }
116
+
117
+ // 如果开头找不到用户消息,尝试从尾部向前找
118
+ if (!firstMessage) {
119
+ for (let i = 0; i < tailLines.length; i++) {
120
+ const line = tailLines[i];
121
+ if (!line.trim()) continue;
122
+
123
+ try {
124
+ const json = JSON.parse(line);
125
+ if (json.type === 'user' && json.message && json.message.content !== 'Warmup') {
126
+ firstMessage = typeof json.message.content === 'string'
127
+ ? json.message.content
128
+ : JSON.stringify(json.message.content);
129
+ break;
130
+ }
131
+ } catch (e) {
132
+ // 忽略
133
+ }
134
+ }
135
+ }
136
+
137
+ // 解析结尾 - 获取最后的对话
138
+ const validTailLines = [];
139
+ for (const line of tailLines) {
140
+ if (!line.trim()) continue;
141
+ try {
142
+ const json = JSON.parse(line);
143
+ if (json.type === 'user' || json.type === 'assistant') {
144
+ validTailLines.push(json);
145
+ }
146
+ } catch (e) {
147
+ // 忽略
148
+ }
149
+ }
150
+
151
+ // 从后往前找最后一条有效消息
152
+ for (let i = validTailLines.length - 1; i >= 0; i--) {
153
+ const json = validTailLines[i];
154
+
155
+ if (!lastMessage && json.type === 'assistant' && json.message && json.message.content) {
156
+ const content = json.message.content;
157
+ if (Array.isArray(content)) {
158
+ for (const item of content) {
159
+ if (item.type === 'text' && item.text) {
160
+ lastMessage = item.text;
161
+ break;
162
+ }
163
+ }
164
+ } else if (typeof content === 'string') {
165
+ lastMessage = content;
166
+ }
167
+ }
168
+
169
+ if (!lastUserMessage && json.type === 'user' && json.message && json.message.content) {
170
+ const content = json.message.content;
171
+ if (typeof content === 'string' && content !== 'Warmup') {
172
+ lastUserMessage = content;
173
+ }
174
+ }
175
+
176
+ if (lastMessage && lastUserMessage) break;
177
+ }
178
+
179
+ // 如果仍然找不到首条消息,使用 summary 或 gitBranch 作为备选
180
+ if (!firstMessage && summary) {
181
+ firstMessage = `[摘要] ${summary}`;
182
+ } else if (!firstMessage && gitBranch) {
183
+ firstMessage = `[分支] ${gitBranch}`;
184
+ }
185
+
186
+ return {
187
+ summary,
188
+ gitBranch,
189
+ firstMessage,
190
+ lastMessage: lastMessage || lastUserMessage, // 优先显示助手回复,否则显示用户消息
191
+ messageCount: 0
192
+ };
193
+ }
194
+
195
+ /**
196
+ * 获取所有可用的项目
197
+ */
198
+ function getAvailableProjects(config) {
199
+ const projectsDir = config.projectsDir;
200
+ if (!fs.existsSync(projectsDir)) {
201
+ console.log(chalk.red(`项目目录不存在: ${projectsDir}`));
202
+ return [];
203
+ }
204
+
205
+ // 获取项目列表和统计信息(包含解析后的名称)
206
+ const { getProjectsWithStats, getProjectOrder } = require('../server/services/sessions');
207
+ const projects = getProjectsWithStats(config);
208
+ const savedOrder = getProjectOrder(config);
209
+
210
+ // 按保存的顺序排列
211
+ let orderedProjects = [];
212
+ if (savedOrder.length > 0) {
213
+ const projectMap = new Map(projects.map(p => [p.name, p]));
214
+ for (const name of savedOrder) {
215
+ if (projectMap.has(name)) {
216
+ orderedProjects.push(projectMap.get(name));
217
+ projectMap.delete(name);
218
+ }
219
+ }
220
+ // 添加不在排序中的新项目
221
+ orderedProjects.push(...projectMap.values());
222
+ } else {
223
+ orderedProjects = projects;
224
+ }
225
+
226
+ // 转换为选项格式
227
+ return orderedProjects.map(project => ({
228
+ name: `${project.displayName} (${project.sessionCount} 会话)`,
229
+ value: project.name,
230
+ }));
231
+ }
232
+
233
+ module.exports = {
234
+ getSessionsDir,
235
+ getAllSessions,
236
+ parseSessionInfoFast,
237
+ parseLinesWithTail,
238
+ getAvailableProjects,
239
+ };