@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.
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Command Permissions API 路由
3
+ *
4
+ * 管理 Claude Code / Codex / Gemini CLI 命令执行权限
5
+ *
6
+ * 三个 CLI 工具的权限控制机制:
7
+ *
8
+ * 1. Claude Code:
9
+ * - 配置文件: ~/.claude/settings.json (用户级) 或 .claude/settings.json (项目级)
10
+ * - 权限格式: permissions.allow / permissions.deny 数组
11
+ * - 支持通配符: Bash(npm run *), Read(./src/**)
12
+ *
13
+ * 2. Codex CLI:
14
+ * - 启动参数: --ask-for-approval never/on-request/on-failure
15
+ * - 沙箱模式: --sandbox read-only/workspace-write/danger-full-access
16
+ * - YOLO 模式: --yolo 或 --dangerously-bypass-approvals-and-sandbox
17
+ *
18
+ * 3. Gemini CLI:
19
+ * - 启动参数: --approval-mode default/auto_edit/yolo
20
+ * - 允许工具: --allowed-tools "ShellTool(git status)"
21
+ * - 沙箱: --sandbox / -s
22
+ */
23
+
24
+ const express = require('express');
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const os = require('os');
28
+
29
+ const router = express.Router();
30
+
31
+ // Claude Code 设置文件路径
32
+ function getClaudeSettingsPath(projectPath, isLocal = false) {
33
+ if (projectPath) {
34
+ return path.join(projectPath, '.claude', isLocal ? 'settings.local.json' : 'settings.json');
35
+ }
36
+ return path.join(os.homedir(), '.claude', 'settings.json');
37
+ }
38
+
39
+ // 读取 Claude Code settings.json
40
+ function readClaudeSettings(projectPath, isLocal = false) {
41
+ const settingsPath = getClaudeSettingsPath(projectPath, isLocal);
42
+ try {
43
+ if (fs.existsSync(settingsPath)) {
44
+ const content = fs.readFileSync(settingsPath, 'utf-8');
45
+ return JSON.parse(content);
46
+ }
47
+ } catch (err) {
48
+ console.error('[Permissions API] Error reading Claude settings:', err);
49
+ }
50
+ return {};
51
+ }
52
+
53
+ // 保存 Claude Code settings.json
54
+ function saveClaudeSettings(projectPath, settings, isLocal = false) {
55
+ const settingsPath = getClaudeSettingsPath(projectPath, isLocal);
56
+ const settingsDir = path.dirname(settingsPath);
57
+
58
+ if (!fs.existsSync(settingsDir)) {
59
+ fs.mkdirSync(settingsDir, { recursive: true });
60
+ }
61
+
62
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
63
+ }
64
+
65
+ // 全局 all-allow 状态(内存中)
66
+ let globalAllAllowEnabled = process.env.CLAUDE_ALL_ALLOW === 'true';
67
+
68
+ /**
69
+ * 获取项目的命令执行权限设置
70
+ * GET /api/permissions
71
+ * Query: projectPath - 项目路径, cliType - CLI 类型 (claude/codex/gemini)
72
+ */
73
+ router.get('/', (req, res) => {
74
+ try {
75
+ const { projectPath, cliType = 'claude' } = req.query;
76
+
77
+ if (!projectPath) {
78
+ return res.status(400).json({
79
+ success: false,
80
+ message: '缺少 projectPath 参数'
81
+ });
82
+ }
83
+
84
+ if (!fs.existsSync(projectPath)) {
85
+ return res.status(404).json({
86
+ success: false,
87
+ message: '项目路径不存在'
88
+ });
89
+ }
90
+
91
+ // 读取 Claude Code 配置(所有 CLI 工具共用此配置管理)
92
+ const settings = readClaudeSettings(projectPath);
93
+ const permissions = settings.permissions || {};
94
+
95
+ res.json({
96
+ success: true,
97
+ cliType,
98
+ settings: {
99
+ // Claude Code 格式
100
+ allow: permissions.allow || [],
101
+ deny: permissions.deny || [],
102
+ // 兼容旧格式
103
+ allowedCommands: permissions.allow || [],
104
+ denyCommands: permissions.deny || []
105
+ }
106
+ });
107
+ } catch (err) {
108
+ console.error('[Permissions API] Get permissions error:', err);
109
+ res.status(500).json({
110
+ success: false,
111
+ message: err.message
112
+ });
113
+ }
114
+ });
115
+
116
+ /**
117
+ * 保存项目的命令执行权限设置
118
+ * POST /api/permissions
119
+ * Body: { projectPath, settings: { allow, deny }, isLocal }
120
+ */
121
+ router.post('/', (req, res) => {
122
+ try {
123
+ const { projectPath, settings: newPermissions, isLocal = false } = req.body;
124
+
125
+ if (!projectPath) {
126
+ return res.status(400).json({
127
+ success: false,
128
+ message: '缺少 projectPath 参数'
129
+ });
130
+ }
131
+
132
+ if (!fs.existsSync(projectPath)) {
133
+ return res.status(404).json({
134
+ success: false,
135
+ message: '项目路径不存在'
136
+ });
137
+ }
138
+
139
+ // 读取现有设置
140
+ const settings = readClaudeSettings(projectPath, isLocal);
141
+
142
+ // 更新权限设置(使用 Claude Code 的标准格式)
143
+ settings.permissions = {
144
+ allow: newPermissions?.allow || newPermissions?.allowedCommands || [],
145
+ deny: newPermissions?.deny || newPermissions?.denyCommands || []
146
+ };
147
+
148
+ // 保存设置
149
+ saveClaudeSettings(projectPath, settings, isLocal);
150
+
151
+ res.json({
152
+ success: true,
153
+ message: '权限设置已保存',
154
+ savedTo: isLocal ? '.claude/settings.local.json' : '.claude/settings.json'
155
+ });
156
+ } catch (err) {
157
+ console.error('[Permissions API] Save permissions error:', err);
158
+ res.status(500).json({
159
+ success: false,
160
+ message: err.message
161
+ });
162
+ }
163
+ });
164
+
165
+ /**
166
+ * 获取全局 all-allow 模式状态
167
+ * GET /api/permissions/all-allow
168
+ */
169
+ router.get('/all-allow', (req, res) => {
170
+ try {
171
+ res.json({
172
+ success: true,
173
+ enabled: globalAllAllowEnabled,
174
+ note: '此设置通过启动命令参数控制,如 claude --dangerously-skip-permissions'
175
+ });
176
+ } catch (err) {
177
+ console.error('[Permissions API] Get all-allow status error:', err);
178
+ res.status(500).json({
179
+ success: false,
180
+ message: err.message
181
+ });
182
+ }
183
+ });
184
+
185
+ /**
186
+ * 设置全局 all-allow 模式(运行时)
187
+ * POST /api/permissions/all-allow
188
+ * Body: { enabled }
189
+ */
190
+ router.post('/all-allow', (req, res) => {
191
+ try {
192
+ const { enabled } = req.body;
193
+ globalAllAllowEnabled = !!enabled;
194
+
195
+ res.json({
196
+ success: true,
197
+ enabled: globalAllAllowEnabled,
198
+ message: enabled ? 'All-Allow 模式已启用' : 'All-Allow 模式已禁用'
199
+ });
200
+ } catch (err) {
201
+ console.error('[Permissions API] Set all-allow status error:', err);
202
+ res.status(500).json({
203
+ success: false,
204
+ message: err.message
205
+ });
206
+ }
207
+ });
208
+
209
+ /**
210
+ * 获取权限模版
211
+ * GET /api/permissions/templates
212
+ */
213
+ router.get('/templates', (req, res) => {
214
+ try {
215
+ const templates = {
216
+ safe: {
217
+ name: '安全模式',
218
+ description: '仅允许只读命令,危险操作需要确认',
219
+ allow: [
220
+ 'Bash(cat:*)',
221
+ 'Bash(ls:*)',
222
+ 'Bash(pwd)',
223
+ 'Bash(echo:*)',
224
+ 'Bash(head:*)',
225
+ 'Bash(tail:*)',
226
+ 'Bash(grep:*)',
227
+ 'Read(*)'
228
+ ],
229
+ deny: [
230
+ 'Bash(rm:*)',
231
+ 'Bash(sudo:*)',
232
+ 'Bash(git push:*)',
233
+ 'Bash(git reset --hard:*)',
234
+ 'Bash(chmod:*)',
235
+ 'Bash(chown:*)',
236
+ 'Edit(*)'
237
+ ]
238
+ },
239
+ balanced: {
240
+ name: '平衡模式',
241
+ description: '允许常用开发命令,危险操作需要确认',
242
+ allow: [
243
+ 'Bash(cat:*)',
244
+ 'Bash(ls:*)',
245
+ 'Bash(pwd)',
246
+ 'Bash(echo:*)',
247
+ 'Bash(head:*)',
248
+ 'Bash(tail:*)',
249
+ 'Bash(grep:*)',
250
+ 'Bash(find:*)',
251
+ 'Bash(git status)',
252
+ 'Bash(git diff:*)',
253
+ 'Bash(git log:*)',
254
+ 'Bash(npm run:*)',
255
+ 'Bash(pnpm:*)',
256
+ 'Bash(yarn:*)',
257
+ 'Read(*)',
258
+ 'Edit(*)'
259
+ ],
260
+ deny: [
261
+ 'Bash(rm -rf:*)',
262
+ 'Bash(sudo:*)',
263
+ 'Bash(git push --force:*)',
264
+ 'Bash(git reset --hard:*)'
265
+ ]
266
+ },
267
+ permissive: {
268
+ name: '宽松模式',
269
+ description: '允许大多数命令,仅阻止极度危险的操作',
270
+ allow: [
271
+ 'Bash(*)',
272
+ 'Read(*)',
273
+ 'Edit(*)'
274
+ ],
275
+ deny: [
276
+ 'Bash(rm -rf /*)',
277
+ 'Bash(sudo rm -rf:*)'
278
+ ]
279
+ }
280
+ };
281
+
282
+ res.json({
283
+ success: true,
284
+ templates
285
+ });
286
+ } catch (err) {
287
+ console.error('[Permissions API] Get templates error:', err);
288
+ res.status(500).json({
289
+ success: false,
290
+ message: err.message
291
+ });
292
+ }
293
+ });
294
+
295
+ /**
296
+ * 获取各 CLI 工具的启动参数配置说明
297
+ * GET /api/permissions/cli-config
298
+ */
299
+ router.get('/cli-config', (req, res) => {
300
+ try {
301
+ const cliConfigs = {
302
+ claude: {
303
+ name: 'Claude Code',
304
+ configFile: '.claude/settings.json',
305
+ userConfigFile: '~/.claude/settings.json',
306
+ permissionFormat: {
307
+ example: {
308
+ permissions: {
309
+ allow: ['Bash(npm run:*)', 'Read(./src/**)'],
310
+ deny: ['Bash(rm:*)', 'Read(.env)']
311
+ }
312
+ }
313
+ },
314
+ allAllowFlag: '--dangerously-skip-permissions',
315
+ docs: 'https://docs.anthropic.com/en/docs/claude-code'
316
+ },
317
+ codex: {
318
+ name: 'Codex CLI',
319
+ sandboxModes: ['read-only', 'workspace-write'],
320
+ approvalModes: ['suggest', 'auto-edit'],
321
+ allAllowFlag: '--dangerously-bypass-approvals-and-sandbox',
322
+ example: 'codex --approval-mode auto-edit --sandbox workspace-write "task"',
323
+ docs: 'https://github.com/openai/codex'
324
+ },
325
+ gemini: {
326
+ name: 'Gemini CLI',
327
+ allowedToolsFlag: '--allowedTools "ShellTool(git status)"',
328
+ sandboxFlag: '--sandbox / -s',
329
+ allAllowFlag: '--yolo',
330
+ docs: 'https://github.com/google-gemini/gemini-cli'
331
+ }
332
+ };
333
+
334
+ res.json({
335
+ success: true,
336
+ cliConfigs
337
+ });
338
+ } catch (err) {
339
+ console.error('[Permissions API] Get CLI config error:', err);
340
+ res.status(500).json({
341
+ success: false,
342
+ message: err.message
343
+ });
344
+ }
345
+ });
346
+
347
+ module.exports = router;
@@ -268,4 +268,192 @@ router.delete('/:scope/*', (req, res) => {
268
268
  }
269
269
  });
270
270
 
271
+ // ==================== 仓库管理 API ====================
272
+
273
+ /**
274
+ * 获取所有规则(包括远程仓库)
275
+ * GET /api/rules/all
276
+ * Query: projectPath, refresh=1 强制刷新缓存
277
+ */
278
+ router.get('/all', async (req, res) => {
279
+ try {
280
+ const { projectPath, refresh } = req.query;
281
+ const forceRefresh = refresh === '1';
282
+ const result = await rulesService.listAllRules(projectPath || null, forceRefresh);
283
+
284
+ res.json({
285
+ success: true,
286
+ ...result
287
+ });
288
+ } catch (err) {
289
+ console.error('[Rules API] List all rules error:', err);
290
+ res.status(500).json({
291
+ success: false,
292
+ message: err.message
293
+ });
294
+ }
295
+ });
296
+
297
+ /**
298
+ * 获取仓库列表
299
+ * GET /api/rules/repos
300
+ */
301
+ router.get('/repos', (req, res) => {
302
+ try {
303
+ const repos = rulesService.getRepos();
304
+ res.json({
305
+ success: true,
306
+ repos
307
+ });
308
+ } catch (err) {
309
+ console.error('[Rules API] Get repos error:', err);
310
+ res.status(500).json({
311
+ success: false,
312
+ message: err.message
313
+ });
314
+ }
315
+ });
316
+
317
+ /**
318
+ * 添加仓库
319
+ * POST /api/rules/repos
320
+ * Body: { owner, name, branch, directory, enabled }
321
+ */
322
+ router.post('/repos', (req, res) => {
323
+ try {
324
+ const { owner, name, branch = 'main', directory = '', enabled = true } = req.body;
325
+
326
+ if (!owner || !name) {
327
+ return res.status(400).json({
328
+ success: false,
329
+ message: 'Missing owner or name'
330
+ });
331
+ }
332
+
333
+ const repos = rulesService.addRepo({ owner, name, branch, directory, enabled });
334
+
335
+ res.json({
336
+ success: true,
337
+ repos
338
+ });
339
+ } catch (err) {
340
+ console.error('[Rules API] Add repo error:', err);
341
+ res.status(500).json({
342
+ success: false,
343
+ message: err.message
344
+ });
345
+ }
346
+ });
347
+
348
+ /**
349
+ * 删除仓库
350
+ * DELETE /api/rules/repos/:owner/:name
351
+ * Query: directory - 可选,子目录路径
352
+ */
353
+ router.delete('/repos/:owner/:name', (req, res) => {
354
+ try {
355
+ const { owner, name } = req.params;
356
+ const { directory = '' } = req.query;
357
+ const repos = rulesService.removeRepo(owner, name, directory);
358
+
359
+ res.json({
360
+ success: true,
361
+ repos
362
+ });
363
+ } catch (err) {
364
+ console.error('[Rules API] Remove repo error:', err);
365
+ res.status(500).json({
366
+ success: false,
367
+ message: err.message
368
+ });
369
+ }
370
+ });
371
+
372
+ /**
373
+ * 切换仓库启用状态
374
+ * PUT /api/rules/repos/:owner/:name/toggle
375
+ * Body: { enabled, directory }
376
+ */
377
+ router.put('/repos/:owner/:name/toggle', (req, res) => {
378
+ try {
379
+ const { owner, name } = req.params;
380
+ const { enabled, directory = '' } = req.body;
381
+
382
+ const repos = rulesService.toggleRepo(owner, name, directory, enabled);
383
+
384
+ res.json({
385
+ success: true,
386
+ repos
387
+ });
388
+ } catch (err) {
389
+ console.error('[Rules API] Toggle repo error:', err);
390
+ res.status(500).json({
391
+ success: false,
392
+ message: err.message
393
+ });
394
+ }
395
+ });
396
+
397
+ /**
398
+ * 从远程仓库安装规则
399
+ * POST /api/rules/install
400
+ * Body: rule object from listAllRules
401
+ */
402
+ router.post('/install', async (req, res) => {
403
+ try {
404
+ const rule = req.body;
405
+
406
+ if (!rule || !rule.repoOwner || !rule.repoName) {
407
+ return res.status(400).json({
408
+ success: false,
409
+ message: 'Missing rule info or repo info'
410
+ });
411
+ }
412
+
413
+ const result = await rulesService.installFromRemote(rule);
414
+
415
+ res.json({
416
+ success: true,
417
+ ...result
418
+ });
419
+ } catch (err) {
420
+ console.error('[Rules API] Install rule error:', err);
421
+ res.status(500).json({
422
+ success: false,
423
+ message: err.message
424
+ });
425
+ }
426
+ });
427
+
428
+ /**
429
+ * 卸载规则
430
+ * POST /api/rules/uninstall
431
+ * Body: { path } - 规则的相对路径
432
+ */
433
+ router.post('/uninstall', (req, res) => {
434
+ try {
435
+ const { path } = req.body;
436
+
437
+ if (!path) {
438
+ return res.status(400).json({
439
+ success: false,
440
+ message: 'Missing path'
441
+ });
442
+ }
443
+
444
+ const result = rulesService.uninstallRule(path);
445
+
446
+ res.json({
447
+ success: true,
448
+ ...result
449
+ });
450
+ } catch (err) {
451
+ console.error('[Rules API] Uninstall rule error:', err);
452
+ res.status(500).json({
453
+ success: false,
454
+ message: err.message
455
+ });
456
+ }
457
+ });
458
+
271
459
  module.exports = router;
@@ -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;