@adversity/coding-tool-x 2.2.0 → 2.3.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.
@@ -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,9 @@ 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
+
146
149
  // 健康检查 API
147
150
  app.use('/api/health-check', require('./api/health-check')(config));
148
151
 
@@ -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
  };
@@ -5,15 +5,28 @@
5
5
  * 命令目录:
6
6
  * - 用户级: ~/.claude/commands/
7
7
  * - 项目级: .claude/commands/
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');
16
+ const {
17
+ parseCommandContent,
18
+ detectCommandFormat,
19
+ convertCommandToCodex,
20
+ convertCommandToClaude,
21
+ parseFrontmatter
22
+ } = require('./format-converter');
13
23
 
14
24
  // 命令目录路径
15
25
  const USER_COMMANDS_DIR = path.join(os.homedir(), '.claude', 'commands');
16
26
 
27
+ // 默认仓库源
28
+ const DEFAULT_REPOS = [];
29
+
17
30
  /**
18
31
  * 确保目录存在
19
32
  */
@@ -24,51 +37,9 @@ function ensureDir(dirPath) {
24
37
  }
25
38
 
26
39
  /**
27
- * 解析 YAML frontmatter
40
+ * 生成 frontmatter 字符串(用于命令创建/更新)
28
41
  */
29
- function parseFrontmatter(content) {
30
- const result = {
31
- frontmatter: {},
32
- body: content
33
- };
34
-
35
- // 移除 BOM
36
- content = content.trim().replace(/^\uFEFF/, '');
37
-
38
- // 解析 YAML frontmatter
39
- const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
40
- if (!match) {
41
- return result;
42
- }
43
-
44
- const frontmatterText = match[1];
45
- result.body = match[2].trim();
46
-
47
- // 简单解析 YAML(支持基本字段)
48
- const lines = frontmatterText.split('\n');
49
- for (const line of lines) {
50
- const colonIndex = line.indexOf(':');
51
- if (colonIndex === -1) continue;
52
-
53
- const key = line.slice(0, colonIndex).trim();
54
- let value = line.slice(colonIndex + 1).trim();
55
-
56
- // 去除引号
57
- if ((value.startsWith('"') && value.endsWith('"')) ||
58
- (value.startsWith("'") && value.endsWith("'"))) {
59
- value = value.slice(1, -1);
60
- }
61
-
62
- result.frontmatter[key] = value;
63
- }
64
-
65
- return result;
66
- }
67
-
68
- /**
69
- * 生成 frontmatter 字符串
70
- */
71
- function generateFrontmatter(data) {
42
+ function generateCommandFrontmatter(data) {
72
43
  const lines = ['---'];
73
44
 
74
45
  if (data.description) {
@@ -80,6 +51,15 @@ function generateFrontmatter(data) {
80
51
  if (data['argument-hint']) {
81
52
  lines.push(`argument-hint: ${data['argument-hint']}`);
82
53
  }
54
+ if (data.model) {
55
+ lines.push(`model: ${data.model}`);
56
+ }
57
+ if (data.context) {
58
+ lines.push(`context: ${data.context}`);
59
+ }
60
+ if (data.agent) {
61
+ lines.push(`agent: ${data.agent}`);
62
+ }
83
63
 
84
64
  lines.push('---');
85
65
  return lines.join('\n');
@@ -141,12 +121,96 @@ function scanCommandsDir(dir, basePath, scope) {
141
121
  return commands;
142
122
  }
143
123
 
124
+ /**
125
+ * Commands 仓库扫描器
126
+ */
127
+ class CommandsRepoScanner extends RepoScannerBase {
128
+ constructor() {
129
+ super({
130
+ type: 'commands',
131
+ installDir: USER_COMMANDS_DIR,
132
+ markerFile: null, // 直接扫描 .md 文件
133
+ fileExtension: '.md',
134
+ defaultRepos: DEFAULT_REPOS
135
+ });
136
+ }
137
+
138
+ /**
139
+ * 获取并解析单个命令文件
140
+ */
141
+ async fetchAndParseItem(file, repo, baseDir) {
142
+ try {
143
+ // 计算相对路径
144
+ const relativePath = baseDir ? file.path.slice(baseDir.length + 1) : file.path;
145
+ const fileName = path.basename(file.path, '.md');
146
+ const namespace = path.dirname(relativePath);
147
+
148
+ // 获取文件内容
149
+ const content = await this.fetchRawContent(repo, file.path);
150
+ const { frontmatter, body } = this.parseFrontmatter(content);
151
+
152
+ return {
153
+ key: `${repo.owner}/${repo.name}:${relativePath}`,
154
+ name: fileName,
155
+ namespace: namespace === '.' ? null : namespace,
156
+ scope: 'remote',
157
+ path: relativePath,
158
+ repoPath: file.path,
159
+ description: frontmatter.description || '',
160
+ allowedTools: frontmatter['allowed-tools'] || '',
161
+ argumentHint: frontmatter['argument-hint'] || '',
162
+ body,
163
+ fullContent: content,
164
+ installed: this.isInstalled(relativePath),
165
+ readmeUrl: `https://github.com/${repo.owner}/${repo.name}/blob/${repo.branch}/${file.path}`,
166
+ repoOwner: repo.owner,
167
+ repoName: repo.name,
168
+ repoBranch: repo.branch,
169
+ repoDirectory: repo.directory || ''
170
+ };
171
+ } catch (err) {
172
+ console.warn(`[CommandsRepoScanner] Parse command ${file.path} error:`, err.message);
173
+ return null;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * 检查命令是否已安装
179
+ */
180
+ isInstalled(relativePath) {
181
+ const fullPath = path.join(this.installDir, relativePath);
182
+ return fs.existsSync(fullPath);
183
+ }
184
+
185
+ /**
186
+ * 获取去重 key
187
+ */
188
+ getDedupeKey(item) {
189
+ // 使用 namespace/name 作为去重 key
190
+ return item.namespace ? `${item.namespace}/${item.name}`.toLowerCase() : item.name.toLowerCase();
191
+ }
192
+
193
+ /**
194
+ * 安装命令
195
+ */
196
+ async installCommand(item) {
197
+ const repo = {
198
+ owner: item.repoOwner,
199
+ name: item.repoName,
200
+ branch: item.repoBranch
201
+ };
202
+
203
+ return this.installFromRepo(item.repoPath, repo, item.path);
204
+ }
205
+ }
206
+
144
207
  /**
145
208
  * Commands 服务类
146
209
  */
147
210
  class CommandsService {
148
211
  constructor() {
149
212
  this.userCommandsDir = USER_COMMANDS_DIR;
213
+ this.repoScanner = new CommandsRepoScanner();
150
214
  ensureDir(this.userCommandsDir);
151
215
  }
152
216
 
@@ -179,6 +243,52 @@ class CommandsService {
179
243
  };
180
244
  }
181
245
 
246
+ /**
247
+ * 获取所有命令(包括远程仓库)
248
+ * @param {boolean} forceRefresh - 强制刷新远程缓存
249
+ */
250
+ async listAllCommands(projectPath = null, forceRefresh = false) {
251
+ // 获取本地命令
252
+ const { commands: localCommands, userCount, projectCount } = this.listCommands(projectPath);
253
+
254
+ // 获取远程命令
255
+ let remoteCommands = [];
256
+ try {
257
+ remoteCommands = await this.repoScanner.listRemoteItems(forceRefresh);
258
+
259
+ // 更新安装状态
260
+ for (const cmd of remoteCommands) {
261
+ cmd.installed = this.repoScanner.isInstalled(cmd.path);
262
+ }
263
+ } catch (err) {
264
+ console.warn('[CommandsService] Failed to fetch remote commands:', err.message);
265
+ }
266
+
267
+ // 合并列表(本地优先)
268
+ const allCommands = [...localCommands];
269
+ const localKeys = new Set(localCommands.map(c =>
270
+ c.namespace ? `${c.namespace}/${c.name}`.toLowerCase() : c.name.toLowerCase()
271
+ ));
272
+
273
+ for (const remote of remoteCommands) {
274
+ const key = remote.namespace ? `${remote.namespace}/${remote.name}`.toLowerCase() : remote.name.toLowerCase();
275
+ if (!localKeys.has(key)) {
276
+ allCommands.push(remote);
277
+ }
278
+ }
279
+
280
+ // 排序
281
+ allCommands.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
282
+
283
+ return {
284
+ commands: allCommands,
285
+ total: allCommands.length,
286
+ userCount,
287
+ projectCount,
288
+ remoteCount: remoteCommands.length
289
+ };
290
+ }
291
+
182
292
  /**
183
293
  * 获取单个命令详情
184
294
  */
@@ -250,7 +360,7 @@ class CommandsService {
250
360
 
251
361
  let content = '';
252
362
  if (Object.keys(frontmatterData).length > 0) {
253
- content = generateFrontmatter(frontmatterData) + '\n\n';
363
+ content = generateCommandFrontmatter(frontmatterData) + '\n\n';
254
364
  }
255
365
  content += body || '';
256
366
 
@@ -285,7 +395,7 @@ class CommandsService {
285
395
 
286
396
  let content = '';
287
397
  if (Object.keys(frontmatterData).length > 0) {
288
- content = generateFrontmatter(frontmatterData) + '\n\n';
398
+ content = generateCommandFrontmatter(frontmatterData) + '\n\n';
289
399
  }
290
400
  content += body || '';
291
401
 
@@ -353,8 +463,82 @@ class CommandsService {
353
463
  namespaces
354
464
  };
355
465
  }
466
+
467
+ // ==================== 仓库管理 ====================
468
+
469
+ /**
470
+ * 获取仓库列表
471
+ */
472
+ getRepos() {
473
+ return this.repoScanner.loadRepos();
474
+ }
475
+
476
+ /**
477
+ * 添加仓库
478
+ */
479
+ addRepo(repo) {
480
+ return this.repoScanner.addRepo(repo);
481
+ }
482
+
483
+ /**
484
+ * 删除仓库
485
+ */
486
+ removeRepo(owner, name, directory = '') {
487
+ return this.repoScanner.removeRepo(owner, name, directory);
488
+ }
489
+
490
+ /**
491
+ * 切换仓库启用状态
492
+ */
493
+ toggleRepo(owner, name, directory = '', enabled) {
494
+ return this.repoScanner.toggleRepo(owner, name, directory, enabled);
495
+ }
496
+
497
+ /**
498
+ * 从远程仓库安装命令
499
+ */
500
+ async installFromRemote(command) {
501
+ return this.repoScanner.installCommand(command);
502
+ }
503
+
504
+ /**
505
+ * 卸载命令
506
+ */
507
+ uninstallCommand(relativePath) {
508
+ return this.repoScanner.uninstall(relativePath);
509
+ }
510
+
511
+ // ==================== 格式转换 ====================
512
+
513
+ /**
514
+ * 转换命令格式
515
+ * @param {string} content - 命令内容
516
+ * @param {string} targetFormat - 目标格式 ('claude' | 'codex')
517
+ */
518
+ convertCommandFormat(content, targetFormat) {
519
+ const sourceFormat = detectCommandFormat(content);
520
+
521
+ if (sourceFormat === targetFormat) {
522
+ return { content, warnings: [], format: targetFormat };
523
+ }
524
+
525
+ if (targetFormat === 'codex') {
526
+ return convertCommandToCodex(content);
527
+ } else {
528
+ return convertCommandToClaude(content);
529
+ }
530
+ }
531
+
532
+ /**
533
+ * 检测命令格式
534
+ * @param {string} content - 命令内容
535
+ */
536
+ detectFormat(content) {
537
+ return detectCommandFormat(content);
538
+ }
356
539
  }
357
540
 
358
541
  module.exports = {
359
- CommandsService
542
+ CommandsService,
543
+ DEFAULT_REPOS
360
544
  };