coding-tool-x 3.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.
- package/CHANGELOG.md +599 -0
- package/LICENSE +21 -0
- package/README.md +439 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
- package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
- package/dist/web/assets/Home-38JTUlYt.js +1 -0
- package/dist/web/assets/Home-CjupSEWE.css +1 -0
- package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
- package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
- package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
- package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
- package/dist/web/assets/icons-DRrXwWZi.js +1 -0
- package/dist/web/assets/index-CetESrXw.css +1 -0
- package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
- package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
- package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +20 -0
- package/dist/web/logo.png +0 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/model-redirection.md +251 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +80 -0
- package/src/commands/channels.js +551 -0
- package/src/commands/cli-type.js +101 -0
- package/src/commands/daemon.js +365 -0
- package/src/commands/doctor.js +333 -0
- package/src/commands/export-config.js +205 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +261 -0
- package/src/commands/plugin.js +585 -0
- package/src/commands/port-config.js +135 -0
- package/src/commands/proxy-control.js +264 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/security.js +37 -0
- package/src/commands/stats.js +398 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +247 -0
- package/src/commands/ui.js +99 -0
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +69 -0
- package/src/config/loader.js +149 -0
- package/src/config/model-metadata.js +167 -0
- package/src/config/model-metadata.json +125 -0
- package/src/config/model-pricing.js +35 -0
- package/src/config/paths.js +190 -0
- package/src/index.js +680 -0
- package/src/plugins/constants.js +15 -0
- package/src/plugins/event-bus.js +54 -0
- package/src/plugins/manifest-validator.js +129 -0
- package/src/plugins/plugin-api.js +128 -0
- package/src/plugins/plugin-installer.js +601 -0
- package/src/plugins/plugin-loader.js +229 -0
- package/src/plugins/plugin-manager.js +170 -0
- package/src/plugins/registry.js +152 -0
- package/src/plugins/schema/plugin-manifest.json +115 -0
- package/src/reset-config.js +94 -0
- package/src/server/api/agents.js +826 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +368 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +417 -0
- package/src/server/api/codex-projects.js +104 -0
- package/src/server/api/codex-proxy.js +195 -0
- package/src/server/api/codex-sessions.js +483 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +482 -0
- package/src/server/api/config-export.js +212 -0
- package/src/server/api/config-registry.js +357 -0
- package/src/server/api/config-sync.js +155 -0
- package/src/server/api/config-templates.js +248 -0
- package/src/server/api/config.js +521 -0
- package/src/server/api/convert.js +260 -0
- package/src/server/api/dashboard.js +142 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +366 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +173 -0
- package/src/server/api/gemini-sessions.js +376 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +31 -0
- package/src/server/api/mcp.js +399 -0
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +327 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +463 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +306 -0
- package/src/server/api/security.js +53 -0
- package/src/server/api/sessions.js +514 -0
- package/src/server/api/settings.js +142 -0
- package/src/server/api/skills.js +570 -0
- package/src/server/api/statistics.js +238 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +456 -0
- package/src/server/codex-proxy-server.js +681 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +610 -0
- package/src/server/index.js +422 -0
- package/src/server/opencode-proxy-server.js +4771 -0
- package/src/server/proxy-server.js +669 -0
- package/src/server/services/agents-service.js +1137 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +240 -0
- package/src/server/services/channels.js +447 -0
- package/src/server/services/codex-channels.js +705 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +936 -0
- package/src/server/services/codex-settings-manager.js +619 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +161 -0
- package/src/server/services/commands-service.js +574 -0
- package/src/server/services/config-export-service.js +1165 -0
- package/src/server/services/config-registry-service.js +828 -0
- package/src/server/services/config-sync-manager.js +941 -0
- package/src/server/services/config-sync-service.js +504 -0
- package/src/server/services/config-templates-service.js +913 -0
- package/src/server/services/enhanced-cache.js +196 -0
- package/src/server/services/env-checker.js +409 -0
- package/src/server/services/env-manager.js +436 -0
- package/src/server/services/favorites.js +165 -0
- package/src/server/services/format-converter.js +620 -0
- package/src/server/services/gemini-channels.js +459 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +157 -0
- package/src/server/services/health-check.js +85 -0
- package/src/server/services/mcp-client.js +790 -0
- package/src/server/services/mcp-service.js +1732 -0
- package/src/server/services/model-detector.js +1245 -0
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +366 -0
- package/src/server/services/opencode-gateway-adapters.js +1168 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -0
- package/src/server/services/opencode-statistics-service.js +161 -0
- package/src/server/services/plugins-service.js +1268 -0
- package/src/server/services/prompts-service.js +534 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/repo-scanner-base.js +708 -0
- package/src/server/services/request-logger.js +130 -0
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +131 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +900 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +1482 -0
- package/src/server/services/speed-test.js +1146 -0
- package/src/server/services/statistics-service.js +1043 -0
- package/src/server/services/ui-config.js +132 -0
- package/src/server/services/workspace-service.js +830 -0
- package/src/server/utils/pricing.js +73 -0
- package/src/server/websocket-server.js +513 -0
- package/src/ui/menu.js +139 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +108 -0
- package/src/utils/session.js +240 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const archiver = require('archiver');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 导出 Claude Code 配置为 ZIP 压缩包
|
|
9
|
+
* 包含:全局 CLAUDE.md、项目 CLAUDE.md、settings.json、MCP 服务器配置、Skills 等
|
|
10
|
+
*/
|
|
11
|
+
async function exportConfig(options = {}) {
|
|
12
|
+
const { output = 'claude-config.zip', includeProjects = true } = options;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
console.log(chalk.blue('🚀 开始导出 Claude Code 配置...'));
|
|
16
|
+
|
|
17
|
+
const homeDir = os.homedir();
|
|
18
|
+
const claudeDir = path.join(homeDir, '.claude');
|
|
19
|
+
const currentDir = process.cwd();
|
|
20
|
+
|
|
21
|
+
// 检查 .claude 目录是否存在
|
|
22
|
+
if (!fs.existsSync(claudeDir)) {
|
|
23
|
+
console.log(chalk.red('❌ 未找到 .claude 配置目录'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 创建输出文件流
|
|
28
|
+
const outputPath = path.resolve(currentDir, output);
|
|
29
|
+
const outputStream = fs.createWriteStream(outputPath);
|
|
30
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
31
|
+
|
|
32
|
+
// 监听错误
|
|
33
|
+
archive.on('error', (err) => {
|
|
34
|
+
throw err;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 监听完成
|
|
38
|
+
archive.on('end', () => {
|
|
39
|
+
console.log(chalk.green(`\n✅ 配置导出成功!`));
|
|
40
|
+
console.log(chalk.gray(` 文件位置: ${outputPath}`));
|
|
41
|
+
console.log(chalk.gray(` 文件大小: ${(archive.pointer() / 1024 / 1024).toFixed(2)} MB`));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// 连接流
|
|
45
|
+
archive.pipe(outputStream);
|
|
46
|
+
|
|
47
|
+
// 收集配置清单
|
|
48
|
+
const manifest = {
|
|
49
|
+
exportTime: new Date().toISOString(),
|
|
50
|
+
version: '1.0.0',
|
|
51
|
+
globalConfig: {},
|
|
52
|
+
projectConfig: {},
|
|
53
|
+
enabledConfigs: []
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// 1. 导出全局 CLAUDE.md
|
|
57
|
+
const globalClaudeMd = path.join(claudeDir, 'CLAUDE.md');
|
|
58
|
+
if (fs.existsSync(globalClaudeMd)) {
|
|
59
|
+
archive.file(globalClaudeMd, { name: 'global/CLAUDE.md' });
|
|
60
|
+
manifest.globalConfig.claudeMd = true;
|
|
61
|
+
manifest.enabledConfigs.push('global/CLAUDE.md');
|
|
62
|
+
console.log(chalk.gray(' ✓ 全局 CLAUDE.md'));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. 导出 settings.json
|
|
66
|
+
const settingsJson = path.join(claudeDir, 'settings.json');
|
|
67
|
+
if (fs.existsSync(settingsJson)) {
|
|
68
|
+
archive.file(settingsJson, { name: 'global/settings.json' });
|
|
69
|
+
manifest.globalConfig.settings = true;
|
|
70
|
+
manifest.enabledConfigs.push('global/settings.json');
|
|
71
|
+
console.log(chalk.gray(' ✓ settings.json'));
|
|
72
|
+
|
|
73
|
+
// 解析 MCP 服务器配置
|
|
74
|
+
try {
|
|
75
|
+
const settings = JSON.parse(fs.readFileSync(settingsJson, 'utf-8'));
|
|
76
|
+
if (settings.mcpServers) {
|
|
77
|
+
manifest.globalConfig.mcpServers = Object.keys(settings.mcpServers);
|
|
78
|
+
console.log(chalk.gray(` - MCP 服务器: ${manifest.globalConfig.mcpServers.join(', ')}`));
|
|
79
|
+
}
|
|
80
|
+
if (settings.hooks) {
|
|
81
|
+
manifest.globalConfig.hooks = Object.keys(settings.hooks);
|
|
82
|
+
}
|
|
83
|
+
if (settings.statusLine) {
|
|
84
|
+
manifest.globalConfig.statusLine = true;
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.log(chalk.yellow(' ⚠ settings.json 解析失败'));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. 导出 Skills
|
|
92
|
+
const skillsDir = path.join(claudeDir, 'skills');
|
|
93
|
+
if (fs.existsSync(skillsDir)) {
|
|
94
|
+
const skills = fs.readdirSync(skillsDir).filter(f => !f.startsWith('.'));
|
|
95
|
+
if (skills.length > 0) {
|
|
96
|
+
archive.directory(skillsDir, 'global/skills');
|
|
97
|
+
manifest.globalConfig.skills = skills;
|
|
98
|
+
manifest.enabledConfigs.push('global/skills');
|
|
99
|
+
console.log(chalk.gray(` ✓ Skills (${skills.length} 个)`));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 4. 导出当前项目配置
|
|
104
|
+
if (includeProjects) {
|
|
105
|
+
// 项目级 CLAUDE.md
|
|
106
|
+
const projectClaudeMd = path.join(currentDir, 'CLAUDE.md');
|
|
107
|
+
if (fs.existsSync(projectClaudeMd)) {
|
|
108
|
+
archive.file(projectClaudeMd, { name: 'project/CLAUDE.md' });
|
|
109
|
+
manifest.projectConfig.claudeMd = true;
|
|
110
|
+
manifest.enabledConfigs.push('project/CLAUDE.md');
|
|
111
|
+
console.log(chalk.gray(' ✓ 项目 CLAUDE.md'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 项目级 settings.local.json
|
|
115
|
+
const projectSettings = path.join(currentDir, '.claude', 'settings.local.json');
|
|
116
|
+
if (fs.existsSync(projectSettings)) {
|
|
117
|
+
archive.file(projectSettings, { name: 'project/.claude/settings.local.json' });
|
|
118
|
+
manifest.projectConfig.settingsLocal = true;
|
|
119
|
+
manifest.enabledConfigs.push('project/.claude/settings.local.json');
|
|
120
|
+
console.log(chalk.gray(' ✓ 项目 settings.local.json'));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 项目级 workspace CLAUDE.md
|
|
124
|
+
const workspaceClaudeMd = path.join(homeDir, 'workspace', 'CLAUDE.md');
|
|
125
|
+
if (fs.existsSync(workspaceClaudeMd)) {
|
|
126
|
+
archive.file(workspaceClaudeMd, { name: 'workspace/CLAUDE.md' });
|
|
127
|
+
manifest.projectConfig.workspaceClaudeMd = true;
|
|
128
|
+
manifest.enabledConfigs.push('workspace/CLAUDE.md');
|
|
129
|
+
console.log(chalk.gray(' ✓ workspace CLAUDE.md'));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 5. 添加配置清单
|
|
134
|
+
archive.append(JSON.stringify(manifest, null, 2), { name: 'manifest.json' });
|
|
135
|
+
console.log(chalk.gray(' ✓ manifest.json'));
|
|
136
|
+
|
|
137
|
+
// 6. 添加 README
|
|
138
|
+
const readme = generateReadme(manifest);
|
|
139
|
+
archive.append(readme, { name: 'README.md' });
|
|
140
|
+
console.log(chalk.gray(' ✓ README.md'));
|
|
141
|
+
|
|
142
|
+
// 完成打包
|
|
143
|
+
await archive.finalize();
|
|
144
|
+
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(chalk.red('❌ 导出失败:'), error.message);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 生成配置包说明文档
|
|
153
|
+
*/
|
|
154
|
+
function generateReadme(manifest) {
|
|
155
|
+
return `# Claude Code 配置包
|
|
156
|
+
|
|
157
|
+
导出时间: ${new Date(manifest.exportTime).toLocaleString('zh-CN')}
|
|
158
|
+
版本: ${manifest.version}
|
|
159
|
+
|
|
160
|
+
## 📦 包含内容
|
|
161
|
+
|
|
162
|
+
### 全局配置
|
|
163
|
+
${manifest.globalConfig.claudeMd ? '- ✅ 全局 CLAUDE.md(AI 行为指令)' : ''}
|
|
164
|
+
${manifest.globalConfig.settings ? '- ✅ settings.json(包含 MCP 服务器、Hooks 等)' : ''}
|
|
165
|
+
${manifest.globalConfig.mcpServers ? ` - MCP 服务器: ${manifest.globalConfig.mcpServers.join(', ')}` : ''}
|
|
166
|
+
${manifest.globalConfig.hooks ? ` - Hooks: ${manifest.globalConfig.hooks.join(', ')}` : ''}
|
|
167
|
+
${manifest.globalConfig.skills ? `- ✅ Skills (${manifest.globalConfig.skills.length} 个): ${manifest.globalConfig.skills.join(', ')}` : ''}
|
|
168
|
+
|
|
169
|
+
### 项目配置
|
|
170
|
+
${manifest.projectConfig.claudeMd ? '- ✅ 项目 CLAUDE.md' : ''}
|
|
171
|
+
${manifest.projectConfig.settingsLocal ? '- ✅ 项目 settings.local.json' : ''}
|
|
172
|
+
${manifest.projectConfig.workspaceClaudeMd ? '- ✅ workspace CLAUDE.md' : ''}
|
|
173
|
+
|
|
174
|
+
## 📥 如何导入
|
|
175
|
+
|
|
176
|
+
### 方法 1:手动导入
|
|
177
|
+
1. 解压此 ZIP 文件
|
|
178
|
+
2. 复制 \`global/\` 内容到 \`~/.claude/\`
|
|
179
|
+
3. 复制 \`project/\` 内容到你的项目目录
|
|
180
|
+
4. 复制 \`workspace/\` 内容到 \`~/workspace/\`
|
|
181
|
+
|
|
182
|
+
### 方法 2:使用命令(计划中)
|
|
183
|
+
\`\`\`bash
|
|
184
|
+
ctx import-config claude-config.zip
|
|
185
|
+
\`\`\`
|
|
186
|
+
|
|
187
|
+
## ⚠️ 注意事项
|
|
188
|
+
|
|
189
|
+
1. **备份现有配置**:导入前请备份现有的 \`.claude\` 目录
|
|
190
|
+
2. **敏感信息**:此配置包可能包含 API Keys、代理配置等敏感信息,请妥善保管
|
|
191
|
+
3. **路径适配**:某些路径可能需要根据你的系统调整(如 hooks 中的脚本路径)
|
|
192
|
+
4. **MCP 服务器**:需要确保目标环境已安装对应的 MCP 服务器
|
|
193
|
+
|
|
194
|
+
## 📝 配置清单
|
|
195
|
+
|
|
196
|
+
已启用的配置项:
|
|
197
|
+
${manifest.enabledConfigs.map(c => `- ${c}`).join('\n')}
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
生成工具: Coding-Tool-X (ctx)
|
|
202
|
+
`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = { exportConfig };
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// 列出会话命令
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const ora = require('ora');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const { getAllSessions, parseSessionInfoFast } = require('../utils/session');
|
|
6
|
+
const { formatTime, formatSize, truncate } = require('../utils/format');
|
|
7
|
+
const { promptSelectSession, promptForkConfirm } = require('../ui/prompts');
|
|
8
|
+
const { resumeSession } = require('./resume');
|
|
9
|
+
const { loadAliases } = require('../server/services/alias');
|
|
10
|
+
const { getRecentSessions } = require('../server/services/sessions');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 列出会话
|
|
14
|
+
*/
|
|
15
|
+
async function listSessions(config, limit = null) {
|
|
16
|
+
const maxSessions = limit || config.maxDisplaySessions;
|
|
17
|
+
const spinner = ora('加载会话列表...').start();
|
|
18
|
+
|
|
19
|
+
const sessions = getAllSessions(config).slice(0, maxSessions);
|
|
20
|
+
|
|
21
|
+
if (sessions.length === 0) {
|
|
22
|
+
spinner.fail('暂无会话记录');
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
spinner.text = '解析会话信息...';
|
|
27
|
+
|
|
28
|
+
// 加载别名
|
|
29
|
+
const aliases = loadAliases();
|
|
30
|
+
|
|
31
|
+
const choices = sessions.map((session, index) => {
|
|
32
|
+
const info = parseSessionInfoFast(session.filePath);
|
|
33
|
+
const time = formatTime(session.mtime);
|
|
34
|
+
const size = formatSize(session.size);
|
|
35
|
+
const alias = aliases[session.sessionId];
|
|
36
|
+
|
|
37
|
+
// 构建显示名称 - 清爽的单行布局
|
|
38
|
+
let displayName = '';
|
|
39
|
+
|
|
40
|
+
// 格式:序号. [别名] 时间 │ 大小 │ 分支 │ 第一条消息
|
|
41
|
+
displayName += chalk.bold.white(`${index + 1}. `);
|
|
42
|
+
|
|
43
|
+
// 如果有别名,优先显示别名
|
|
44
|
+
if (alias) {
|
|
45
|
+
displayName += chalk.yellow.bold(`[${alias}] `);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
displayName += chalk.cyan(`${time.padEnd(10)}`);
|
|
49
|
+
displayName += chalk.gray(` │ ${size.padEnd(9)}`);
|
|
50
|
+
|
|
51
|
+
if (info.gitBranch) {
|
|
52
|
+
const branchName = info.gitBranch
|
|
53
|
+
.replace('feature/', '')
|
|
54
|
+
.replace('feat/', '')
|
|
55
|
+
.replace('fix/', '')
|
|
56
|
+
.substring(0, 25);
|
|
57
|
+
displayName += chalk.green(` │ ${branchName.padEnd(25)}`);
|
|
58
|
+
} else {
|
|
59
|
+
displayName += chalk.gray(` │ ${''.padEnd(25)}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 只显示第一条用户消息(你说明这个会话是干嘛的)
|
|
63
|
+
if (info.firstMessage && info.firstMessage !== 'Warmup') {
|
|
64
|
+
const firstMsg = truncate(info.firstMessage, 50);
|
|
65
|
+
displayName += chalk.gray(' │ ') + chalk.white(firstMsg);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
name: displayName,
|
|
70
|
+
value: session.sessionId,
|
|
71
|
+
short: alias ? `${alias} (${session.sessionId.substring(0, 8)})` : `会话 ${session.sessionId.substring(0, 8)}`,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
spinner.stop();
|
|
76
|
+
spinner.clear();
|
|
77
|
+
|
|
78
|
+
// 清屏并重新显示,避免之前的输出干扰
|
|
79
|
+
console.clear();
|
|
80
|
+
console.log(chalk.green(`\n✨ 找到 ${sessions.length} 个会话\n`));
|
|
81
|
+
|
|
82
|
+
return choices;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 列出跨项目的最近会话
|
|
87
|
+
*/
|
|
88
|
+
async function listRecentSessionsAcrossProjects(config, limit = null) {
|
|
89
|
+
const maxSessions = limit || 15; // 默认显示15条最新对话
|
|
90
|
+
const spinner = ora('加载最新对话...').start();
|
|
91
|
+
|
|
92
|
+
const sessions = await getRecentSessions(config, maxSessions);
|
|
93
|
+
|
|
94
|
+
if (sessions.length === 0) {
|
|
95
|
+
spinner.fail('暂无会话记录');
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
spinner.text = '解析会话信息...';
|
|
100
|
+
|
|
101
|
+
const choices = sessions.map((session, index) => {
|
|
102
|
+
const time = formatTime(session.mtime);
|
|
103
|
+
const size = formatSize(session.size);
|
|
104
|
+
const alias = session.alias;
|
|
105
|
+
|
|
106
|
+
// 构建显示名称 - 清爽的单行布局
|
|
107
|
+
let displayName = '';
|
|
108
|
+
|
|
109
|
+
// 格式:序号. [项目名] [别名] 时间 │ 大小 │ 分支 │ 第一条消息
|
|
110
|
+
displayName += chalk.bold.white(`${index + 1}. `);
|
|
111
|
+
|
|
112
|
+
// 项目名(高亮显示)
|
|
113
|
+
const projectName = session.projectDisplayName || session.projectName;
|
|
114
|
+
displayName += chalk.magenta.bold(`[${projectName}] `);
|
|
115
|
+
|
|
116
|
+
// 如果有别名,显示别名
|
|
117
|
+
if (alias) {
|
|
118
|
+
displayName += chalk.yellow.bold(`[${alias}] `);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
displayName += chalk.cyan(`${time.padEnd(10)}`);
|
|
122
|
+
displayName += chalk.gray(` │ ${size.padEnd(9)}`);
|
|
123
|
+
|
|
124
|
+
if (session.gitBranch) {
|
|
125
|
+
const branchName = session.gitBranch
|
|
126
|
+
.replace('feature/', '')
|
|
127
|
+
.replace('feat/', '')
|
|
128
|
+
.replace('fix/', '')
|
|
129
|
+
.substring(0, 25);
|
|
130
|
+
displayName += chalk.green(` │ ${branchName.padEnd(25)}`);
|
|
131
|
+
} else {
|
|
132
|
+
displayName += chalk.gray(` │ ${''.padEnd(25)}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 只显示第一条用户消息
|
|
136
|
+
if (session.firstMessage && session.firstMessage !== 'Warmup') {
|
|
137
|
+
const firstMsg = truncate(session.firstMessage, 50);
|
|
138
|
+
displayName += chalk.gray(' │ ') + chalk.white(firstMsg);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
name: displayName,
|
|
143
|
+
value: { sessionId: session.sessionId, projectName: session.projectName },
|
|
144
|
+
short: alias ? `${alias} (${session.sessionId.substring(0, 8)})` : `会话 ${session.sessionId.substring(0, 8)}`,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
spinner.stop();
|
|
149
|
+
spinner.clear();
|
|
150
|
+
|
|
151
|
+
// 清屏并重新显示,避免之前的输出干扰
|
|
152
|
+
console.clear();
|
|
153
|
+
console.log(chalk.green(`\n✨ 找到 ${sessions.length} 个最新对话(跨所有项目)\n`));
|
|
154
|
+
|
|
155
|
+
return choices;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 处理列出会话
|
|
160
|
+
*/
|
|
161
|
+
async function handleList(config, switchProjectCallback, crossProject = false) {
|
|
162
|
+
while (true) {
|
|
163
|
+
// 根据模式选择不同的列表函数
|
|
164
|
+
const choices = crossProject
|
|
165
|
+
? await listRecentSessionsAcrossProjects(config)
|
|
166
|
+
: await listSessions(config);
|
|
167
|
+
|
|
168
|
+
if (choices.length === 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 添加操作选项
|
|
173
|
+
choices.push(new inquirer.Separator(chalk.gray('─'.repeat(50))));
|
|
174
|
+
choices.push({ name: chalk.blue('↩️ 返回主菜单'), value: 'back' });
|
|
175
|
+
|
|
176
|
+
// 跨项目模式不显示切换项目选项(因为已经是跨所有项目了)
|
|
177
|
+
if (!crossProject) {
|
|
178
|
+
choices.push({ name: chalk.magenta('🔀 切换项目'), value: 'switch' });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const selected = await promptSelectSession(choices);
|
|
182
|
+
|
|
183
|
+
if (selected === 'back') {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (selected === 'switch') {
|
|
188
|
+
const switched = await switchProjectCallback();
|
|
189
|
+
if (!switched) {
|
|
190
|
+
return; // 用户取消切换,返回主菜单
|
|
191
|
+
}
|
|
192
|
+
continue; // 切换后重新加载列表
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 跨项目模式:selected 是 { sessionId, projectName }
|
|
196
|
+
// 单项目模式:selected 是 sessionId
|
|
197
|
+
let sessionId, projectName;
|
|
198
|
+
if (crossProject) {
|
|
199
|
+
sessionId = selected.sessionId;
|
|
200
|
+
projectName = selected.projectName;
|
|
201
|
+
// 切换到该项目
|
|
202
|
+
config.currentProject = projectName;
|
|
203
|
+
} else {
|
|
204
|
+
sessionId = selected;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 询问是否 fork
|
|
208
|
+
const action = await promptForkConfirm();
|
|
209
|
+
|
|
210
|
+
if (action === 'back') {
|
|
211
|
+
continue; // 返回列表重新选择
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const fork = action === 'fork';
|
|
215
|
+
await resumeSession(config, sessionId, fork);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
listSessions,
|
|
221
|
+
handleList,
|
|
222
|
+
};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const LOGS_DIR = path.join(os.homedir(), '.cc-tool', 'logs');
|
|
8
|
+
|
|
9
|
+
const LOG_FILES = {
|
|
10
|
+
ui: 'cc-tool-out.log',
|
|
11
|
+
claude: 'claude-proxy.log',
|
|
12
|
+
codex: 'codex-proxy.log',
|
|
13
|
+
gemini: 'gemini-proxy.log',
|
|
14
|
+
opencode: 'opencode-proxy.log'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 确保日志目录存在
|
|
19
|
+
*/
|
|
20
|
+
function ensureLogsDir() {
|
|
21
|
+
if (!fs.existsSync(LOGS_DIR)) {
|
|
22
|
+
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 查看日志
|
|
28
|
+
*/
|
|
29
|
+
async function handleLogs(type = null, options = {}) {
|
|
30
|
+
ensureLogsDir();
|
|
31
|
+
|
|
32
|
+
const lines = options.lines || 50;
|
|
33
|
+
const follow = options.follow || false;
|
|
34
|
+
const clear = options.clear || false;
|
|
35
|
+
|
|
36
|
+
// 如果是清空日志
|
|
37
|
+
if (clear) {
|
|
38
|
+
return clearLogs(type);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 如果没有指定类型,显示所有日志
|
|
42
|
+
if (!type) {
|
|
43
|
+
return showAllLogs(lines, follow);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 显示特定类型的日志
|
|
47
|
+
const logFile = LOG_FILES[type];
|
|
48
|
+
if (!logFile) {
|
|
49
|
+
console.error(chalk.red(`\n❌ 无效的日志类型: ${type}\n`));
|
|
50
|
+
console.log(chalk.gray('支持的类型: ui, claude, codex, gemini, opencode\n'));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const logPath = path.join(LOGS_DIR, logFile);
|
|
55
|
+
|
|
56
|
+
// 检查日志文件是否存在
|
|
57
|
+
if (!fs.existsSync(logPath)) {
|
|
58
|
+
console.log(chalk.yellow(`\n⚠️ ${type} 日志文件不存在\n`));
|
|
59
|
+
console.log(chalk.gray(`日志路径: ${logPath}\n`));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(chalk.cyan(`\n📋 ${type.toUpperCase()} 日志 ${follow ? '(实时)' : `(最近 ${lines} 行)`}\n`));
|
|
64
|
+
console.log(chalk.gray(`═`.repeat(60)) + '\n');
|
|
65
|
+
|
|
66
|
+
if (follow) {
|
|
67
|
+
// 实时跟踪日志
|
|
68
|
+
tailFile(logPath);
|
|
69
|
+
} else {
|
|
70
|
+
// 显示最后 N 行
|
|
71
|
+
showLastLines(logPath, lines);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 显示所有日志(合并)
|
|
77
|
+
*/
|
|
78
|
+
function showAllLogs(lines, follow) {
|
|
79
|
+
console.log(chalk.cyan(`\n📋 所有日志 ${follow ? '(实时)' : `(最近 ${lines} 行)`}\n`));
|
|
80
|
+
console.log(chalk.gray(`═`.repeat(60)) + '\n');
|
|
81
|
+
|
|
82
|
+
const allLogs = [];
|
|
83
|
+
|
|
84
|
+
// 读取所有日志文件
|
|
85
|
+
Object.entries(LOG_FILES).forEach(([type, filename]) => {
|
|
86
|
+
const logPath = path.join(LOGS_DIR, filename);
|
|
87
|
+
if (fs.existsSync(logPath)) {
|
|
88
|
+
try {
|
|
89
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
90
|
+
const logLines = content.trim().split('\n').filter(line => line.trim());
|
|
91
|
+
|
|
92
|
+
logLines.forEach(line => {
|
|
93
|
+
allLogs.push({
|
|
94
|
+
type,
|
|
95
|
+
line,
|
|
96
|
+
// 尝试从日志中提取时间戳
|
|
97
|
+
timestamp: extractTimestamp(line) || Date.now()
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
} catch (err) {
|
|
101
|
+
// 忽略读取错误
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// 按时间戳排序
|
|
107
|
+
allLogs.sort((a, b) => a.timestamp - b.timestamp);
|
|
108
|
+
|
|
109
|
+
// 只显示最后 N 行
|
|
110
|
+
const recentLogs = allLogs.slice(-lines);
|
|
111
|
+
|
|
112
|
+
recentLogs.forEach(log => {
|
|
113
|
+
const typeColor = getTypeColor(log.type);
|
|
114
|
+
const typeLabel = `[${log.type.toUpperCase()}]`.padEnd(10);
|
|
115
|
+
console.log(typeColor(typeLabel) + chalk.gray(log.line));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
console.log(chalk.gray(`\n═`.repeat(60)));
|
|
119
|
+
console.log(chalk.gray(`\n💡 使用 `) + chalk.cyan(`ctx logs ${Object.keys(LOG_FILES).join('|')}`) + chalk.gray(` 查看特定类型日志\n`));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 显示文件最后 N 行
|
|
124
|
+
*/
|
|
125
|
+
function showLastLines(filePath, lines) {
|
|
126
|
+
try {
|
|
127
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
128
|
+
const allLines = content.trim().split('\n');
|
|
129
|
+
const lastLines = allLines.slice(-lines);
|
|
130
|
+
|
|
131
|
+
lastLines.forEach(line => {
|
|
132
|
+
if (line.trim()) {
|
|
133
|
+
console.log(line);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
console.log(chalk.gray(`\n═`.repeat(60)));
|
|
138
|
+
console.log(chalk.gray(`\n💡 使用 `) + chalk.cyan(`ctx logs <type> --follow`) + chalk.gray(` 实时跟踪日志\n`));
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error(chalk.red(`读取日志失败: ${err.message}\n`));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 实时跟踪日志文件
|
|
147
|
+
*/
|
|
148
|
+
function tailFile(filePath) {
|
|
149
|
+
console.log(chalk.gray('按 Ctrl+C 停止跟踪\n'));
|
|
150
|
+
|
|
151
|
+
const tail = spawn('tail', ['-f', filePath]);
|
|
152
|
+
|
|
153
|
+
tail.stdout.on('data', (data) => {
|
|
154
|
+
process.stdout.write(data.toString());
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
tail.stderr.on('data', (data) => {
|
|
158
|
+
process.stderr.write(chalk.red(data.toString()));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
tail.on('error', (err) => {
|
|
162
|
+
console.error(chalk.red(`\n❌ 跟踪日志失败: ${err.message}\n`));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// 处理退出信号
|
|
167
|
+
process.on('SIGINT', () => {
|
|
168
|
+
tail.kill();
|
|
169
|
+
console.log(chalk.gray('\n\n已停止跟踪日志\n'));
|
|
170
|
+
process.exit(0);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 清空日志
|
|
176
|
+
*/
|
|
177
|
+
function clearLogs(type) {
|
|
178
|
+
if (!type) {
|
|
179
|
+
// 清空所有日志
|
|
180
|
+
console.log(chalk.cyan('\n🗑️ 清空所有日志...\n'));
|
|
181
|
+
|
|
182
|
+
let cleared = 0;
|
|
183
|
+
Object.entries(LOG_FILES).forEach(([logType, filename]) => {
|
|
184
|
+
const logPath = path.join(LOGS_DIR, filename);
|
|
185
|
+
if (fs.existsSync(logPath)) {
|
|
186
|
+
try {
|
|
187
|
+
fs.writeFileSync(logPath, '');
|
|
188
|
+
console.log(chalk.green(`✅ ${logType} 日志已清空`));
|
|
189
|
+
cleared++;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.log(chalk.red(`❌ ${logType} 日志清空失败: ${err.message}`));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
console.log(chalk.green(`\n✅ 共清空 ${cleared} 个日志文件\n`));
|
|
197
|
+
} else {
|
|
198
|
+
// 清空特定类型日志
|
|
199
|
+
const logFile = LOG_FILES[type];
|
|
200
|
+
if (!logFile) {
|
|
201
|
+
console.error(chalk.red(`\n❌ 无效的日志类型: ${type}\n`));
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const logPath = path.join(LOGS_DIR, logFile);
|
|
206
|
+
if (fs.existsSync(logPath)) {
|
|
207
|
+
try {
|
|
208
|
+
fs.writeFileSync(logPath, '');
|
|
209
|
+
console.log(chalk.green(`\n✅ ${type} 日志已清空\n`));
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error(chalk.red(`\n❌ 清空失败: ${err.message}\n`));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
console.log(chalk.yellow(`\n⚠️ ${type} 日志文件不存在\n`));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 从日志行中提取时间戳
|
|
222
|
+
*/
|
|
223
|
+
function extractTimestamp(line) {
|
|
224
|
+
// 尝试匹配常见的时间戳格式
|
|
225
|
+
const patterns = [
|
|
226
|
+
/^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/, // YYYY-MM-DD HH:MM:SS
|
|
227
|
+
/^\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})/, // [YYYY-MM-DDTHH:MM:SS
|
|
228
|
+
/^(\d{2}:\d{2}:\d{2})/ // HH:MM:SS
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
for (const pattern of patterns) {
|
|
232
|
+
const match = line.match(pattern);
|
|
233
|
+
if (match) {
|
|
234
|
+
try {
|
|
235
|
+
return new Date(match[1]).getTime();
|
|
236
|
+
} catch (err) {
|
|
237
|
+
// 忽略解析错误
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 获取类型颜色
|
|
247
|
+
*/
|
|
248
|
+
function getTypeColor(type) {
|
|
249
|
+
const colors = {
|
|
250
|
+
ui: chalk.blue,
|
|
251
|
+
claude: chalk.green,
|
|
252
|
+
codex: chalk.cyan,
|
|
253
|
+
gemini: chalk.magenta,
|
|
254
|
+
opencode: chalk.yellow
|
|
255
|
+
};
|
|
256
|
+
return colors[type] || chalk.gray;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
handleLogs
|
|
261
|
+
};
|