@adversity/coding-tool-x 2.2.0 → 2.4.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 (34) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +12 -14
  3. package/dist/web/assets/index-Bu1oPcKu.js +4009 -0
  4. package/dist/web/assets/index-XSok7-mN.css +41 -0
  5. package/dist/web/index.html +2 -2
  6. package/package.json +5 -4
  7. package/src/config/default.js +1 -1
  8. package/src/index.js +2 -2
  9. package/src/server/api/agents.js +188 -0
  10. package/src/server/api/commands.js +261 -0
  11. package/src/server/api/config-export.js +122 -0
  12. package/src/server/api/config-templates.js +26 -5
  13. package/src/server/api/health-check.js +1 -89
  14. package/src/server/api/permissions.js +370 -0
  15. package/src/server/api/rules.js +188 -0
  16. package/src/server/api/skills.js +66 -14
  17. package/src/server/api/workspaces.js +30 -55
  18. package/src/server/index.js +7 -11
  19. package/src/server/services/agents-service.js +179 -1
  20. package/src/server/services/commands-service.js +231 -47
  21. package/src/server/services/config-export-service.js +209 -0
  22. package/src/server/services/config-templates-service.js +481 -107
  23. package/src/server/services/format-converter.js +506 -0
  24. package/src/server/services/health-check.js +1 -315
  25. package/src/server/services/permission-templates-service.js +339 -0
  26. package/src/server/services/repo-scanner-base.js +678 -0
  27. package/src/server/services/rules-service.js +179 -1
  28. package/src/server/services/skill-service.js +114 -61
  29. package/src/server/services/workspace-service.js +52 -1
  30. package/dist/web/assets/index-D1AYlFLZ.js +0 -3220
  31. package/dist/web/assets/index-aL3cKxSK.css +0 -41
  32. package/docs/CHANGELOG.md +0 -582
  33. package/docs/DIRECTORY_MIGRATION.md +0 -112
  34. package/docs/PROJECT_STRUCTURE.md +0 -396
@@ -83,11 +83,13 @@ router.get('/installed', (req, res) => {
83
83
  /**
84
84
  * 安装技能
85
85
  * POST /api/skills/install
86
- * Body: { directory, repo: { owner, name, branch } }
86
+ * Body: { directory, fullDirectory, repo: { owner, name, branch } }
87
+ * - directory: 本地安装目录(相对路径)
88
+ * - fullDirectory: 仓库中的完整路径(当指定了仓库子目录时使用)
87
89
  */
88
90
  router.post('/install', async (req, res) => {
89
91
  try {
90
- const { directory, repo } = req.body;
92
+ const { directory, fullDirectory, repo } = req.body;
91
93
 
92
94
  if (!directory) {
93
95
  return res.status(400).json({
@@ -103,11 +105,15 @@ router.post('/install', async (req, res) => {
103
105
  });
104
106
  }
105
107
 
106
- const result = await skillService.installSkill(directory, {
107
- owner: repo.owner,
108
- name: repo.name,
109
- branch: repo.branch || 'main'
110
- });
108
+ const result = await skillService.installSkill(
109
+ directory,
110
+ {
111
+ owner: repo.owner,
112
+ name: repo.name,
113
+ branch: repo.branch || 'main'
114
+ },
115
+ fullDirectory || null // 传递 fullDirectory 用于从仓库子目录下载
116
+ );
111
117
 
112
118
  res.json({
113
119
  success: true,
@@ -227,11 +233,12 @@ router.get('/repos', (req, res) => {
227
233
  /**
228
234
  * 添加仓库
229
235
  * POST /api/skills/repos
230
- * Body: { owner, name, branch, enabled }
236
+ * Body: { owner, name, branch, directory, enabled }
237
+ * - directory: 可选,指定扫描的子目录路径
231
238
  */
232
239
  router.post('/repos', (req, res) => {
233
240
  try {
234
- const { owner, name, branch = 'main', enabled = true } = req.body;
241
+ const { owner, name, branch = 'main', directory = '', enabled = true } = req.body;
235
242
 
236
243
  if (!owner || !name) {
237
244
  return res.status(400).json({
@@ -240,7 +247,7 @@ router.post('/repos', (req, res) => {
240
247
  });
241
248
  }
242
249
 
243
- const repos = skillService.addRepo({ owner, name, branch, enabled });
250
+ const repos = skillService.addRepo({ owner, name, branch, directory, enabled });
244
251
 
245
252
  res.json({
246
253
  success: true,
@@ -258,11 +265,13 @@ router.post('/repos', (req, res) => {
258
265
  /**
259
266
  * 删除仓库
260
267
  * DELETE /api/skills/repos/:owner/:name
268
+ * Query: directory - 可选,子目录路径
261
269
  */
262
270
  router.delete('/repos/:owner/:name', (req, res) => {
263
271
  try {
264
272
  const { owner, name } = req.params;
265
- const repos = skillService.removeRepo(owner, name);
273
+ const { directory = '' } = req.query;
274
+ const repos = skillService.removeRepo(owner, name, directory);
266
275
 
267
276
  res.json({
268
277
  success: true,
@@ -280,14 +289,15 @@ router.delete('/repos/:owner/:name', (req, res) => {
280
289
  /**
281
290
  * 切换仓库启用状态
282
291
  * PUT /api/skills/repos/:owner/:name/toggle
283
- * Body: { enabled }
292
+ * Body: { enabled, directory }
293
+ * - directory: 可选,子目录路径
284
294
  */
285
295
  router.put('/repos/:owner/:name/toggle', (req, res) => {
286
296
  try {
287
297
  const { owner, name } = req.params;
288
- const { enabled } = req.body;
298
+ const { enabled, directory = '' } = req.body;
289
299
 
290
- const repos = skillService.toggleRepo(owner, name, enabled);
300
+ const repos = skillService.toggleRepo(owner, name, directory, enabled);
291
301
 
292
302
  res.json({
293
303
  success: true,
@@ -302,4 +312,46 @@ router.put('/repos/:owner/:name/toggle', (req, res) => {
302
312
  }
303
313
  });
304
314
 
315
+ // ==================== 格式转换 API ====================
316
+
317
+ /**
318
+ * 转换技能格式
319
+ * POST /api/skills/convert
320
+ * Body: { content, targetFormat }
321
+ * - content: 技能内容
322
+ * - targetFormat: 目标格式 ('claude' | 'codex')
323
+ */
324
+ router.post('/convert', (req, res) => {
325
+ try {
326
+ const { content, targetFormat } = req.body;
327
+
328
+ if (!content) {
329
+ return res.status(400).json({
330
+ success: false,
331
+ message: '请提供技能内容'
332
+ });
333
+ }
334
+
335
+ if (!['claude', 'codex'].includes(targetFormat)) {
336
+ return res.status(400).json({
337
+ success: false,
338
+ message: '目标格式必须是 claude 或 codex'
339
+ });
340
+ }
341
+
342
+ const result = skillService.convertSkillFormat(content, targetFormat);
343
+
344
+ res.json({
345
+ success: true,
346
+ ...result
347
+ });
348
+ } catch (err) {
349
+ console.error('[Skills API] Convert skill error:', err);
350
+ res.status(500).json({
351
+ success: false,
352
+ message: err.message
353
+ });
354
+ }
355
+ });
356
+
305
357
  module.exports = router;
@@ -105,12 +105,38 @@ router.get('/check-git/*', (req, res) => {
105
105
  }
106
106
  });
107
107
 
108
+ /**
109
+ * GET /api/workspaces/available-projects
110
+ * 获取所有渠道(Claude/Codex/Gemini)的项目并集
111
+ */
112
+ router.get('/available-projects', (req, res) => {
113
+ try {
114
+ const projects = workspaceService.getAllAvailableProjects();
115
+ res.json({
116
+ success: true,
117
+ data: projects
118
+ });
119
+ } catch (error) {
120
+ res.status(500).json({
121
+ success: false,
122
+ message: error.message
123
+ });
124
+ }
125
+ });
126
+
108
127
  /**
109
128
  * GET /api/workspaces/:id
110
129
  * 获取单个工作区详情
111
130
  */
112
- router.get('/:id', (req, res) => {
131
+ // 注意:此路由需要放在所有静态子路由之后,避免把 /available-projects 等路径当成 id
132
+ router.get('/:id', (req, res, next) => {
113
133
  try {
134
+ // 兜底:即便路由顺序被改动,也避免把保留路径当成工作区 ID
135
+ const reservedIds = new Set(['available-projects', 'read-file']);
136
+ if (reservedIds.has(req.params.id)) {
137
+ return next();
138
+ }
139
+
114
140
  const workspace = workspaceService.getWorkspace(req.params.id);
115
141
  if (!workspace) {
116
142
  return res.status(404).json({
@@ -147,7 +173,7 @@ router.get('/:id', (req, res) => {
147
173
  */
148
174
  router.post('/', (req, res) => {
149
175
  try {
150
- const { name, description, baseDir, projects, configTemplateId } = req.body;
176
+ const { name, description, baseDir, projects, configTemplateId, permissionTemplate } = req.body;
151
177
 
152
178
  if (!name || !name.trim()) {
153
179
  return res.status(400).json({
@@ -185,7 +211,8 @@ router.post('/', (req, res) => {
185
211
  description,
186
212
  baseDir,
187
213
  projects,
188
- configTemplateId
214
+ configTemplateId,
215
+ permissionTemplate
189
216
  });
190
217
 
191
218
  res.json({
@@ -322,25 +349,6 @@ router.delete('/:id/projects/:projectName', (req, res) => {
322
349
  }
323
350
  });
324
351
 
325
- /**
326
- * GET /api/workspaces/available-projects
327
- * 获取所有渠道(Claude/Codex/Gemini)的项目并集
328
- */
329
- router.get('/available-projects', (req, res) => {
330
- try {
331
- const projects = workspaceService.getAllAvailableProjects();
332
- res.json({
333
- success: true,
334
- data: projects
335
- });
336
- } catch (error) {
337
- res.status(500).json({
338
- success: false,
339
- message: error.message
340
- });
341
- }
342
- });
343
-
344
352
  /**
345
353
  * POST /api/workspaces/:id/launch
346
354
  * 获取在工作区启动 CLI 工具的命令
@@ -371,37 +379,4 @@ router.post('/:id/launch', (req, res) => {
371
379
  }
372
380
  });
373
381
 
374
- /**
375
- * GET /api/workspaces/check-git/:projectPath
376
- * 检查项目是否是 git 仓库并获取 worktrees
377
- */
378
- router.get('/check-git/*', (req, res) => {
379
- try {
380
- const projectPath = req.params[0];
381
-
382
- if (!projectPath) {
383
- return res.status(400).json({
384
- success: false,
385
- message: '项目路径不能为空'
386
- });
387
- }
388
-
389
- const isGit = workspaceService.isGitRepo(projectPath);
390
- const worktrees = isGit ? workspaceService.getGitWorktrees(projectPath) : [];
391
-
392
- res.json({
393
- success: true,
394
- data: {
395
- isGitRepo: isGit,
396
- worktrees
397
- }
398
- });
399
- } catch (error) {
400
- res.status(500).json({
401
- success: false,
402
- message: error.message
403
- });
404
- }
405
- });
406
-
407
382
  module.exports = router;
@@ -143,6 +143,12 @@ async function startServer(port) {
143
143
  // 配置模板 API
144
144
  app.use('/api/config-templates', require('./api/config-templates'));
145
145
 
146
+ // 命令执行权限 API
147
+ app.use('/api/permissions', require('./api/permissions'));
148
+
149
+ // 配置导出/导入 API
150
+ app.use('/api/config-export', require('./api/config-export'));
151
+
146
152
  // 健康检查 API
147
153
  app.use('/api/health-check', require('./api/health-check')(config));
148
154
 
@@ -256,7 +262,7 @@ function autoRestoreProxies() {
256
262
 
257
263
  // 启动时执行健康检查
258
264
  function performStartupHealthCheck() {
259
- const { healthCheckAllProjects, scanLegacySessionFiles } = require('./services/health-check');
265
+ const { healthCheckAllProjects } = require('./services/health-check');
260
266
  const { getProjects } = require('./services/sessions');
261
267
 
262
268
  try {
@@ -286,16 +292,6 @@ function performStartupHealthCheck() {
286
292
  console.log(chalk.green(` ✓ 所有 ${healthResult.summary.healthy} 个项目状态正常`));
287
293
  }
288
294
 
289
- // 扫描旧文件
290
- const legacyResult = scanLegacySessionFiles();
291
-
292
- if (legacyResult.found && legacyResult.projectCount > 0) {
293
- console.log(chalk.yellow(`\n ⚠ 发现 ${legacyResult.projectCount} 个项目的旧会话文件在全局目录`));
294
- console.log(chalk.gray(' 💡 提示: 可通过 Web UI 或 API 清理这些文件'));
295
- console.log(chalk.gray(` - Web UI: 设置 -> 系统维护 -> 清理旧文件`));
296
- console.log(chalk.gray(` - API: POST /api/health-check/clean-legacy`));
297
- }
298
-
299
295
  console.log('');
300
296
  } catch (err) {
301
297
  console.error(chalk.red(' ✗ 健康检查失败:'), err.message);
@@ -5,15 +5,21 @@
5
5
  * 代理目录:
6
6
  * - 用户级: ~/.claude/agents/
7
7
  * - 项目级: .claude/agents/
8
+ *
9
+ * 支持从 GitHub 仓库扫描和安装代理
8
10
  */
9
11
 
10
12
  const fs = require('fs');
11
13
  const path = require('path');
12
14
  const os = require('os');
15
+ const { RepoScannerBase } = require('./repo-scanner-base');
13
16
 
14
17
  // 代理目录路径
15
18
  const USER_AGENTS_DIR = path.join(os.homedir(), '.claude', 'agents');
16
19
 
20
+ // 默认仓库源
21
+ const DEFAULT_REPOS = [];
22
+
17
23
  /**
18
24
  * 确保目录存在
19
25
  */
@@ -154,12 +160,97 @@ function scanAgentsDir(dir, basePath, scope) {
154
160
  return agents;
155
161
  }
156
162
 
163
+ /**
164
+ * Agents 仓库扫描器
165
+ */
166
+ class AgentsRepoScanner extends RepoScannerBase {
167
+ constructor() {
168
+ super({
169
+ type: 'agents',
170
+ installDir: USER_AGENTS_DIR,
171
+ markerFile: null, // 直接扫描 .md 文件
172
+ fileExtension: '.md',
173
+ defaultRepos: DEFAULT_REPOS
174
+ });
175
+ }
176
+
177
+ /**
178
+ * 获取并解析单个代理文件
179
+ */
180
+ async fetchAndParseItem(file, repo, baseDir) {
181
+ try {
182
+ // 计算相对路径
183
+ const relativePath = baseDir ? file.path.slice(baseDir.length + 1) : file.path;
184
+ const fileName = path.basename(file.path, '.md');
185
+
186
+ // 获取文件内容
187
+ const content = await this.fetchRawContent(repo, file.path);
188
+ const { frontmatter, body } = this.parseFrontmatter(content);
189
+
190
+ return {
191
+ key: `${repo.owner}/${repo.name}:${relativePath}`,
192
+ name: frontmatter.name || fileName,
193
+ fileName,
194
+ scope: 'remote',
195
+ path: relativePath,
196
+ repoPath: file.path,
197
+ description: frontmatter.description || '',
198
+ tools: frontmatter.tools || '',
199
+ model: frontmatter.model || '',
200
+ permissionMode: frontmatter.permissionMode || '',
201
+ skills: frontmatter.skills || '',
202
+ systemPrompt: body,
203
+ fullContent: content,
204
+ installed: this.isInstalled(fileName),
205
+ readmeUrl: `https://github.com/${repo.owner}/${repo.name}/blob/${repo.branch}/${file.path}`,
206
+ repoOwner: repo.owner,
207
+ repoName: repo.name,
208
+ repoBranch: repo.branch,
209
+ repoDirectory: repo.directory || ''
210
+ };
211
+ } catch (err) {
212
+ console.warn(`[AgentsRepoScanner] Parse agent ${file.path} error:`, err.message);
213
+ return null;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * 检查代理是否已安装
219
+ */
220
+ isInstalled(fileName) {
221
+ const fullPath = path.join(this.installDir, `${fileName}.md`);
222
+ return fs.existsSync(fullPath);
223
+ }
224
+
225
+ /**
226
+ * 获取去重 key
227
+ */
228
+ getDedupeKey(item) {
229
+ return item.fileName.toLowerCase();
230
+ }
231
+
232
+ /**
233
+ * 安装代理
234
+ */
235
+ async installAgent(item) {
236
+ const repo = {
237
+ owner: item.repoOwner,
238
+ name: item.repoName,
239
+ branch: item.repoBranch
240
+ };
241
+
242
+ // 代理安装到根目录,使用文件名
243
+ return this.installFromRepo(item.repoPath, repo, `${item.fileName}.md`);
244
+ }
245
+ }
246
+
157
247
  /**
158
248
  * Agents 服务类
159
249
  */
160
250
  class AgentsService {
161
251
  constructor() {
162
252
  this.userAgentsDir = USER_AGENTS_DIR;
253
+ this.repoScanner = new AgentsRepoScanner();
163
254
  ensureDir(this.userAgentsDir);
164
255
  }
165
256
 
@@ -192,6 +283,48 @@ class AgentsService {
192
283
  };
193
284
  }
194
285
 
286
+ /**
287
+ * 获取所有代理(包括远程仓库)
288
+ */
289
+ async listAllAgents(projectPath = null, forceRefresh = false) {
290
+ // 获取本地代理
291
+ const { agents: localAgents, userCount, projectCount } = this.listAgents(projectPath);
292
+
293
+ // 获取远程代理
294
+ let remoteAgents = [];
295
+ try {
296
+ remoteAgents = await this.repoScanner.listRemoteItems(forceRefresh);
297
+
298
+ // 更新安装状态
299
+ for (const agent of remoteAgents) {
300
+ agent.installed = this.repoScanner.isInstalled(agent.fileName);
301
+ }
302
+ } catch (err) {
303
+ console.warn('[AgentsService] Failed to fetch remote agents:', err.message);
304
+ }
305
+
306
+ // 合并列表(本地优先)
307
+ const allAgents = [...localAgents];
308
+ const localKeys = new Set(localAgents.map(a => a.fileName.toLowerCase()));
309
+
310
+ for (const remote of remoteAgents) {
311
+ if (!localKeys.has(remote.fileName.toLowerCase())) {
312
+ allAgents.push(remote);
313
+ }
314
+ }
315
+
316
+ // 排序
317
+ allAgents.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
318
+
319
+ return {
320
+ agents: allAgents,
321
+ total: allAgents.length,
322
+ userCount,
323
+ projectCount,
324
+ remoteCount: remoteAgents.length
325
+ };
326
+ }
327
+
195
328
  /**
196
329
  * 获取单个代理详情
197
330
  */
@@ -347,8 +480,53 @@ class AgentsService {
347
480
  models
348
481
  };
349
482
  }
483
+
484
+ // ==================== 仓库管理 ====================
485
+
486
+ /**
487
+ * 获取仓库列表
488
+ */
489
+ getRepos() {
490
+ return this.repoScanner.loadRepos();
491
+ }
492
+
493
+ /**
494
+ * 添加仓库
495
+ */
496
+ addRepo(repo) {
497
+ return this.repoScanner.addRepo(repo);
498
+ }
499
+
500
+ /**
501
+ * 删除仓库
502
+ */
503
+ removeRepo(owner, name, directory = '') {
504
+ return this.repoScanner.removeRepo(owner, name, directory);
505
+ }
506
+
507
+ /**
508
+ * 切换仓库启用状态
509
+ */
510
+ toggleRepo(owner, name, directory = '', enabled) {
511
+ return this.repoScanner.toggleRepo(owner, name, directory, enabled);
512
+ }
513
+
514
+ /**
515
+ * 从远程仓库安装代理
516
+ */
517
+ async installFromRemote(agent) {
518
+ return this.repoScanner.installAgent(agent);
519
+ }
520
+
521
+ /**
522
+ * 卸载代理
523
+ */
524
+ uninstallAgent(fileName) {
525
+ return this.repoScanner.uninstall(`${fileName}.md`);
526
+ }
350
527
  }
351
528
 
352
529
  module.exports = {
353
- AgentsService
530
+ AgentsService,
531
+ DEFAULT_REPOS
354
532
  };