coding-tool-x 3.5.8 → 3.5.10

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 (91) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +4 -25
  3. package/bin/ctx.js +1 -6
  4. package/dist/web/assets/{Analytics-BzoNzfbi.js → Analytics-DPb1zErn.js} +2 -2
  5. package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
  6. package/dist/web/assets/{ConfigTemplates-O4ikBt1o.js → ConfigTemplates-Bki2k4Lo.js} +1 -1
  7. package/dist/web/assets/{Home-BQjsnblU.js → Home-BKYHKSax.js} +1 -1
  8. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  9. package/dist/web/assets/{PluginManager-DS_DJnVc.js → PluginManager-jgeQOE24.js} +1 -1
  10. package/dist/web/assets/ProjectList-CF67RzP_.js +1 -0
  11. package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
  12. package/dist/web/assets/SessionList-DhHsY5q9.js +1 -0
  13. package/dist/web/assets/SessionList-lz23U6ZJ.css +1 -0
  14. package/dist/web/assets/{SkillManager-DpNE02r0.js → SkillManager-CtEz5gGp.js} +1 -1
  15. package/dist/web/assets/{WorkspaceManager-DMY7_SHh.js → WorkspaceManager-D3B0Y5XE.js} +1 -1
  16. package/dist/web/assets/icons-lKCTySl7.js +1 -0
  17. package/dist/web/assets/index-BS7aplYp.css +1 -0
  18. package/dist/web/assets/index-BnyNm74G.js +2 -0
  19. package/dist/web/assets/{markdown-DyTJGI4N.js → markdown-BNJfEgCA.js} +1 -1
  20. package/dist/web/assets/{naive-ui-Cg4_ZeoT.js → naive-ui-DM897mIP.js} +1 -1
  21. package/dist/web/assets/vendors-DQA4Hw9v.js +7 -0
  22. package/dist/web/assets/vue-vendor-B5RqjEjQ.js +45 -0
  23. package/dist/web/index.html +8 -8
  24. package/package.json +1 -2
  25. package/src/commands/daemon.js +5 -33
  26. package/src/commands/export-config.js +6 -6
  27. package/src/commands/stats.js +4 -41
  28. package/src/commands/ui.js +3 -12
  29. package/src/config/channel-preset-websites.js +114 -0
  30. package/src/config/default.js +9 -5
  31. package/src/config/loader.js +2 -2
  32. package/src/config/model-metadata.json +16 -20
  33. package/src/config/paths.js +33 -166
  34. package/src/index.js +34 -125
  35. package/src/server/api/agents.js +2 -52
  36. package/src/server/api/codex-proxy.js +4 -2
  37. package/src/server/api/codex-sessions.js +88 -9
  38. package/src/server/api/commands.js +2 -38
  39. package/src/server/api/dashboard.js +1 -25
  40. package/src/server/api/gemini-proxy.js +4 -2
  41. package/src/server/api/gemini-sessions.js +86 -9
  42. package/src/server/api/hooks.js +1 -17
  43. package/src/server/api/opencode-proxy.js +7 -2
  44. package/src/server/api/opencode-sessions.js +82 -7
  45. package/src/server/api/plugins.js +34 -128
  46. package/src/server/api/proxy.js +5 -3
  47. package/src/server/api/sessions.js +180 -24
  48. package/src/server/api/workspaces.js +0 -14
  49. package/src/server/index.js +9 -31
  50. package/src/server/services/agents-service.js +62 -269
  51. package/src/server/services/base/base-channel-service.js +3 -1
  52. package/src/server/services/channels.js +5 -0
  53. package/src/server/services/codex-channels.js +2 -0
  54. package/src/server/services/codex-sessions.js +78 -108
  55. package/src/server/services/commands-service.js +81 -281
  56. package/src/server/services/config-export-service.js +7 -7
  57. package/src/server/services/config-registry-service.js +9 -8
  58. package/src/server/services/config-sync-manager.js +41 -61
  59. package/src/server/services/config-sync-service.js +3 -3
  60. package/src/server/services/gemini-channels.js +17 -6
  61. package/src/server/services/gemini-config.js +4 -3
  62. package/src/server/services/gemini-sessions.js +51 -26
  63. package/src/server/services/gemini-settings-manager.js +3 -2
  64. package/src/server/services/mcp-service.js +14 -9
  65. package/src/server/services/native-oauth-adapters.js +3 -3
  66. package/src/server/services/network-access.js +2 -61
  67. package/src/server/services/notification-hooks.js +19 -184
  68. package/src/server/services/opencode-channels.js +9 -0
  69. package/src/server/services/opencode-sessions.js +4 -4
  70. package/src/server/services/opencode-settings-manager.js +3 -3
  71. package/src/server/services/plugins-service.js +62 -996
  72. package/src/server/services/prompts-service.js +9 -5
  73. package/src/server/services/sessions.js +128 -99
  74. package/src/server/services/skill-service.js +18 -155
  75. package/src/server/websocket-server.js +4 -35
  76. package/dist/web/assets/Analytics-vQS5IWvs.css +0 -1
  77. package/dist/web/assets/Home-qzk118Of.css +0 -1
  78. package/dist/web/assets/ProjectList-CqYDtsHx.js +0 -1
  79. package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
  80. package/dist/web/assets/SessionList-CfPtcq6Y.css +0 -1
  81. package/dist/web/assets/SessionList-DMlLtMCz.js +0 -1
  82. package/dist/web/assets/icons-CEq2hYB-.js +0 -1
  83. package/dist/web/assets/index-Clf0l3wc.js +0 -2
  84. package/dist/web/assets/index-Dih_bOsv.css +0 -1
  85. package/dist/web/assets/vendors-Bsp-dq2d.js +0 -7
  86. package/dist/web/assets/vue-vendor-BxIT0uQq.js +0 -45
  87. package/docs/Caddyfile.example +0 -19
  88. package/docs/reverse-proxy-https.md +0 -57
  89. package/src/server/services/https-cert.js +0 -171
  90. package/src/server/services/session-launch-command.js +0 -81
  91. package/src/server/services/web-ui-runtime.js +0 -54
@@ -8,7 +8,7 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const { RepoScannerBase } = require('./repo-scanner-base');
11
- const { NATIVE_PATHS, PATHS } = require('../../config/paths');
11
+ const { NATIVE_PATHS } = require('../../config/paths');
12
12
  const {
13
13
  parseCommandContent,
14
14
  parseFrontmatter
@@ -17,21 +17,18 @@ const {
17
17
  // 默认仓库源
18
18
  const DEFAULT_REPOS = [];
19
19
  const SUPPORTED_PLATFORMS = ['claude', 'opencode'];
20
- const CLAUDE_COMMANDS_DIR = NATIVE_PATHS.claude.commands;
21
- const OPENCODE_COMMANDS_DIR = NATIVE_PATHS.opencode.commands;
22
- const OPENCODE_LEGACY_COMMANDS_DIR = NATIVE_PATHS.opencode.commandsLegacy;
20
+ const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
21
+ const CLAUDE_COMMANDS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'commands');
23
22
 
24
23
  const PLATFORM_CONFIG = {
25
24
  claude: {
26
25
  userCommandsDir: CLAUDE_COMMANDS_DIR,
27
- storageDir: PATHS.localCommands.claude,
28
26
  projectCommandsDir: (projectPath) => path.join(projectPath, '.claude', 'commands'),
29
27
  repoType: 'commands'
30
28
  },
31
29
  opencode: {
32
- userCommandsDir: OPENCODE_COMMANDS_DIR,
33
- storageDir: PATHS.localCommands.opencode,
34
- legacyUserCommandsDir: OPENCODE_LEGACY_COMMANDS_DIR,
30
+ userCommandsDir: path.join(OPENCODE_CONFIG_DIR, 'commands'),
31
+ legacyUserCommandsDir: path.join(OPENCODE_CONFIG_DIR, 'command'),
35
32
  projectCommandsDir: (projectPath) => {
36
33
  const modern = path.join(projectPath, '.opencode', 'commands');
37
34
  const legacy = path.join(projectPath, '.opencode', 'command');
@@ -57,25 +54,6 @@ function ensureDir(dirPath) {
57
54
  }
58
55
  }
59
56
 
60
- function normalizeCommandRelativePath(relativePath, errorLabel = '命令路径') {
61
- const raw = String(relativePath || '').replace(/\\/g, '/').trim();
62
- if (!raw || raw.includes('\0')) {
63
- throw new Error(`${errorLabel}不合法`);
64
- }
65
-
66
- const normalized = path.posix.normalize(raw).replace(/^(\.\/)+/, '');
67
- if (!normalized ||
68
- normalized === '.' ||
69
- normalized === '..' ||
70
- normalized.startsWith('../') ||
71
- normalized.includes('/../') ||
72
- path.posix.isAbsolute(normalized)) {
73
- throw new Error(`${errorLabel}不合法`);
74
- }
75
-
76
- return normalized;
77
- }
78
-
79
57
  /**
80
58
  * 生成 frontmatter 字符串(用于命令创建/更新)
81
59
  */
@@ -111,7 +89,7 @@ function generateCommandFrontmatter(data) {
111
89
  /**
112
90
  * 递归扫描目录获取命令文件
113
91
  */
114
- function scanCommandsDir(dir, basePath, scope, options = {}) {
92
+ function scanCommandsDir(dir, basePath, scope) {
115
93
  const commands = [];
116
94
 
117
95
  if (!fs.existsSync(dir)) {
@@ -126,7 +104,7 @@ function scanCommandsDir(dir, basePath, scope, options = {}) {
126
104
 
127
105
  if (entry.isDirectory() && !entry.name.startsWith('.')) {
128
106
  // 递归扫描子目录
129
- const subCommands = scanCommandsDir(fullPath, basePath, scope, options);
107
+ const subCommands = scanCommandsDir(fullPath, basePath, scope);
130
108
  commands.push(...subCommands);
131
109
  } else if (entry.isFile() && entry.name.endsWith('.md')) {
132
110
  // 解析命令文件
@@ -135,10 +113,11 @@ function scanCommandsDir(dir, basePath, scope, options = {}) {
135
113
  const { frontmatter, body } = parseFrontmatter(content);
136
114
 
137
115
  // 计算相对路径和命令名
138
- const relativePath = normalizeCommandRelativePath(path.relative(basePath, fullPath));
116
+ const relativePath = path.relative(basePath, fullPath);
139
117
  const commandName = entry.name.replace(/\.md$/, '');
140
118
  const namespace = path.dirname(relativePath);
141
- const command = {
119
+
120
+ commands.push({
142
121
  name: commandName,
143
122
  namespace: namespace === '.' ? null : namespace,
144
123
  scope,
@@ -153,15 +132,7 @@ function scanCommandsDir(dir, basePath, scope, options = {}) {
153
132
  body,
154
133
  fullContent: content,
155
134
  updatedAt: fs.statSync(fullPath).mtime.getTime()
156
- };
157
- const decoratedCommand = typeof options.decorate === 'function'
158
- ? {
159
- ...command,
160
- ...options.decorate(command)
161
- }
162
- : command;
163
-
164
- commands.push(decoratedCommand);
135
+ });
165
136
  } catch (err) {
166
137
  console.warn(`[CommandsService] Failed to parse ${fullPath}:`, err.message);
167
138
  }
@@ -269,7 +240,6 @@ class CommandsService {
269
240
  const config = PLATFORM_CONFIG[this.platform];
270
241
 
271
242
  this.userCommandsDir = config.userCommandsDir;
272
- this.storageDir = config.storageDir;
273
243
  if (this.platform === 'opencode') {
274
244
  const legacyUserDir = config.legacyUserCommandsDir;
275
245
  if (legacyUserDir && fs.existsSync(legacyUserDir) && !fs.existsSync(this.userCommandsDir)) {
@@ -280,7 +250,6 @@ class CommandsService {
280
250
  this.projectCommandsDir = config.projectCommandsDir;
281
251
  this.repoScanner = new CommandsRepoScanner(this.platform, this.userCommandsDir);
282
252
  ensureDir(this.userCommandsDir);
283
- ensureDir(this.storageDir);
284
253
  }
285
254
 
286
255
  getProjectCommandsDir(projectPath) {
@@ -292,165 +261,28 @@ class CommandsService {
292
261
  * 获取所有命令列表
293
262
  * @param {string} projectPath - 项目路径(可选,用于获取项目级命令)
294
263
  */
295
- getManagedCommandPath(relativePath) {
296
- return path.join(this.storageDir, normalizeCommandRelativePath(relativePath));
297
- }
298
-
299
- isInstalledCommand(relativePath) {
300
- return fs.existsSync(path.join(this.userCommandsDir, normalizeCommandRelativePath(relativePath)));
301
- }
302
-
303
- buildCommandContent({ description, allowedTools, argumentHint, agent, model, subtask, body }) {
304
- const frontmatterData = {};
305
- if (description) frontmatterData.description = description;
306
- if (this.platform !== 'opencode') {
307
- if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
308
- if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
309
- }
310
- if (agent) frontmatterData.agent = agent;
311
- if (model) frontmatterData.model = model;
312
- if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
313
-
314
- let content = '';
315
- if (Object.keys(frontmatterData).length > 0) {
316
- content = generateCommandFrontmatter(frontmatterData) + '\n\n';
317
- }
318
- content += body || '';
319
- return content;
320
- }
321
-
322
- copyFileRecursive(sourcePath, targetPath) {
323
- ensureDir(path.dirname(targetPath));
324
- fs.copyFileSync(sourcePath, targetPath);
325
- }
326
-
327
- ensureManagedCommandCopy(relativePath, sourcePath, options = {}) {
328
- const overwrite = options.overwrite === true;
329
- const normalizedPath = normalizeCommandRelativePath(relativePath);
330
- if (!sourcePath || !fs.existsSync(sourcePath)) {
331
- return false;
332
- }
333
-
334
- const managedPath = this.getManagedCommandPath(normalizedPath);
335
- if (fs.existsSync(managedPath) && !overwrite) {
336
- return false;
337
- }
338
-
339
- this.copyFileRecursive(sourcePath, managedPath);
340
- return true;
341
- }
342
-
343
- removeEmptyCommandDirs(baseDir, relativePath) {
344
- const normalizedPath = normalizeCommandRelativePath(relativePath);
345
- let currentDir = path.dirname(path.join(baseDir, normalizedPath));
346
- const resolvedBaseDir = path.resolve(baseDir);
347
-
348
- while (currentDir && currentDir !== resolvedBaseDir && currentDir.startsWith(`${resolvedBaseDir}${path.sep}`)) {
349
- try {
350
- if (fs.existsSync(currentDir) && fs.readdirSync(currentDir).length === 0) {
351
- fs.rmdirSync(currentDir);
352
- currentDir = path.dirname(currentDir);
353
- continue;
354
- }
355
- } catch {
356
- // ignore cleanup failures
357
- }
358
- break;
359
- }
360
- }
361
-
362
- mergeInstalledUserCommands(commands, options = {}) {
363
- const syncManagedLocalCommands = options.syncManagedLocalCommands === true;
364
- const installedCommands = scanCommandsDir(this.userCommandsDir, this.userCommandsDir, 'user', {
365
- decorate: () => ({
366
- installed: true,
367
- isManagedLocal: false,
368
- source: 'native-installed'
369
- })
370
- });
371
-
372
- for (const installedCommand of installedCommands) {
373
- const existing = commands.find(command =>
374
- command.scope === 'user' &&
375
- command.path.toLowerCase() === installedCommand.path.toLowerCase()
376
- );
377
- const managedPath = this.getManagedCommandPath(installedCommand.path);
378
-
379
- if (existing) {
380
- if (syncManagedLocalCommands && fs.existsSync(managedPath)) {
381
- this.ensureManagedCommandCopy(installedCommand.path, installedCommand.fullPath, { overwrite: true });
382
- }
383
- existing.installed = true;
384
- if (!existing.description && installedCommand.description) existing.description = installedCommand.description;
385
- if (!existing.body && installedCommand.body) existing.body = installedCommand.body;
386
- if (!existing.fullContent && installedCommand.fullContent) existing.fullContent = installedCommand.fullContent;
387
- continue;
388
- }
389
-
390
- this.ensureManagedCommandCopy(installedCommand.path, installedCommand.fullPath, {
391
- overwrite: syncManagedLocalCommands
392
- });
393
- commands.push(installedCommand);
394
- }
395
- }
396
-
397
- mergeLocalUserCommands(commands) {
398
- if (!fs.existsSync(this.storageDir)) return;
399
-
400
- const localCommands = scanCommandsDir(this.storageDir, this.storageDir, 'user', {
401
- decorate: (command) => ({
402
- installed: this.isInstalledCommand(command.path),
403
- isManagedLocal: true,
404
- source: 'local'
405
- })
406
- });
407
-
408
- for (const localCommand of localCommands) {
409
- const existing = commands.find(command =>
410
- command.scope === 'user' &&
411
- command.path.toLowerCase() === localCommand.path.toLowerCase()
412
- );
413
-
414
- if (existing) {
415
- existing.isManagedLocal = true;
416
- existing.installed = this.isInstalledCommand(existing.path);
417
- continue;
418
- }
419
-
420
- commands.push(localCommand);
421
- }
422
- }
423
-
424
- listCommands(projectPath = null, options = {}) {
264
+ listCommands(projectPath = null) {
425
265
  const commands = [];
426
- const syncManagedLocalCommands = options.syncManagedLocalCommands === true;
427
266
 
428
- this.mergeInstalledUserCommands(commands, { syncManagedLocalCommands });
429
- this.mergeLocalUserCommands(commands);
267
+ // 获取用户级命令
268
+ const userCommands = scanCommandsDir(this.userCommandsDir, this.userCommandsDir, 'user');
269
+ commands.push(...userCommands);
430
270
 
431
271
  // 获取项目级命令(如果提供了项目路径)
432
272
  if (projectPath) {
433
273
  const projectCommandsDir = this.getProjectCommandsDir(projectPath);
434
- const projectCommands = scanCommandsDir(projectCommandsDir, projectCommandsDir, 'project', {
435
- decorate: () => ({
436
- installed: true,
437
- isManagedLocal: false,
438
- source: 'native-installed'
439
- })
440
- });
274
+ const projectCommands = scanCommandsDir(projectCommandsDir, projectCommandsDir, 'project');
441
275
  commands.push(...projectCommands);
442
276
  }
443
277
 
444
278
  // 按名称排序
445
279
  commands.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
446
- const userCount = commands.filter(command => command.scope === 'user').length;
447
- const projectCount = commands.length - userCount;
448
280
 
449
281
  return {
450
282
  commands,
451
283
  total: commands.length,
452
- userCount,
453
- projectCount
284
+ userCount: userCommands.length,
285
+ projectCount: commands.length - userCommands.length
454
286
  };
455
287
  }
456
288
 
@@ -460,9 +292,7 @@ class CommandsService {
460
292
  */
461
293
  async listAllCommands(projectPath = null, forceRefresh = false) {
462
294
  // 获取本地命令
463
- const { commands: localCommands, userCount, projectCount } = this.listCommands(projectPath, {
464
- syncManagedLocalCommands: forceRefresh
465
- });
295
+ const { commands: localCommands, userCount, projectCount } = this.listCommands(projectPath);
466
296
 
467
297
  // 获取远程命令
468
298
  let remoteCommands = [];
@@ -510,30 +340,25 @@ class CommandsService {
510
340
  ? this.userCommandsDir
511
341
  : this.getProjectCommandsDir(projectPath);
512
342
 
513
- const relativePath = normalizeCommandRelativePath(namespace
343
+ const relativePath = namespace
514
344
  ? path.join(namespace, `${name}.md`)
515
- : `${name}.md`);
345
+ : `${name}.md`;
346
+
516
347
  const fullPath = path.join(baseDir, relativePath);
517
- const managedPath = scope === 'user' ? this.getManagedCommandPath(relativePath) : '';
518
- const activePath = fs.existsSync(fullPath)
519
- ? fullPath
520
- : (managedPath && fs.existsSync(managedPath) ? managedPath : '');
521
348
 
522
- if (!activePath) {
349
+ if (!fs.existsSync(fullPath)) {
523
350
  return null;
524
351
  }
525
352
 
526
- const content = fs.readFileSync(activePath, 'utf-8');
353
+ const content = fs.readFileSync(fullPath, 'utf-8');
527
354
  const { frontmatter, body } = parseFrontmatter(content);
528
- const installed = fs.existsSync(fullPath);
529
- const isManagedLocal = scope === 'user' && !!managedPath && fs.existsSync(managedPath);
530
355
 
531
356
  return {
532
357
  name,
533
358
  namespace,
534
359
  scope,
535
360
  path: relativePath,
536
- fullPath: activePath,
361
+ fullPath,
537
362
  description: frontmatter.description || '',
538
363
  allowedTools: frontmatter['allowed-tools'] || '',
539
364
  argumentHint: frontmatter['argument-hint'] || '',
@@ -542,10 +367,7 @@ class CommandsService {
542
367
  subtask: frontmatter.subtask || '',
543
368
  body,
544
369
  fullContent: content,
545
- installed,
546
- isManagedLocal,
547
- source: installed ? 'native-installed' : 'local',
548
- updatedAt: fs.statSync(activePath).mtime.getTime()
370
+ updatedAt: fs.statSync(fullPath).mtime.getTime()
549
371
  };
550
372
  }
551
373
 
@@ -570,28 +392,30 @@ class CommandsService {
570
392
  ensureDir(targetDir);
571
393
 
572
394
  const filePath = path.join(targetDir, `${name}.md`);
573
- const relativePath = normalizeCommandRelativePath(namespace ? path.join(namespace, `${name}.md`) : `${name}.md`);
574
- const managedPath = scope === 'user' ? this.getManagedCommandPath(relativePath) : '';
575
395
 
576
396
  // 检查是否已存在
577
- if (fs.existsSync(filePath) || (managedPath && fs.existsSync(managedPath))) {
397
+ if (fs.existsSync(filePath)) {
578
398
  throw new Error(`命令 "${name}" 已存在`);
579
399
  }
580
400
 
581
- const content = this.buildCommandContent({
582
- description,
583
- allowedTools,
584
- argumentHint,
585
- agent,
586
- model,
587
- subtask,
588
- body
589
- });
401
+ // 生成文件内容
402
+ const frontmatterData = {};
403
+ if (description) frontmatterData.description = description;
404
+ if (this.platform !== 'opencode') {
405
+ if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
406
+ if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
407
+ }
408
+ if (agent) frontmatterData.agent = agent;
409
+ if (model) frontmatterData.model = model;
410
+ if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
590
411
 
591
- fs.writeFileSync(filePath, content, 'utf-8');
592
- if (managedPath) {
593
- this.copyFileRecursive(filePath, managedPath);
412
+ let content = '';
413
+ if (Object.keys(frontmatterData).length > 0) {
414
+ content = generateCommandFrontmatter(frontmatterData) + '\n\n';
594
415
  }
416
+ content += body || '';
417
+
418
+ fs.writeFileSync(filePath, content, 'utf-8');
595
419
 
596
420
  return this.getCommand(name, scope, projectPath, namespace);
597
421
  }
@@ -604,34 +428,34 @@ class CommandsService {
604
428
  ? this.userCommandsDir
605
429
  : this.getProjectCommandsDir(projectPath);
606
430
 
607
- const relativePath = normalizeCommandRelativePath(namespace
431
+ const relativePath = namespace
608
432
  ? path.join(namespace, `${name}.md`)
609
- : `${name}.md`);
433
+ : `${name}.md`;
434
+
610
435
  const filePath = path.join(baseDir, relativePath);
611
- const managedPath = scope === 'user' ? this.getManagedCommandPath(relativePath) : '';
612
- const hasManagedCopy = managedPath && fs.existsSync(managedPath);
613
436
 
614
- if (!fs.existsSync(filePath) && !hasManagedCopy) {
437
+ if (!fs.existsSync(filePath)) {
615
438
  throw new Error(`命令 "${name}" 不存在`);
616
439
  }
617
440
 
618
- const content = this.buildCommandContent({
619
- description,
620
- allowedTools,
621
- argumentHint,
622
- agent,
623
- model,
624
- subtask,
625
- body
626
- });
627
-
628
- if (fs.existsSync(filePath)) {
629
- fs.writeFileSync(filePath, content, 'utf-8');
441
+ // 生成文件内容
442
+ const frontmatterData = {};
443
+ if (description) frontmatterData.description = description;
444
+ if (this.platform !== 'opencode') {
445
+ if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
446
+ if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
630
447
  }
631
- if (managedPath) {
632
- ensureDir(path.dirname(managedPath));
633
- fs.writeFileSync(managedPath, content, 'utf-8');
448
+ if (agent) frontmatterData.agent = agent;
449
+ if (model) frontmatterData.model = model;
450
+ if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
451
+
452
+ let content = '';
453
+ if (Object.keys(frontmatterData).length > 0) {
454
+ content = generateCommandFrontmatter(frontmatterData) + '\n\n';
634
455
  }
456
+ content += body || '';
457
+
458
+ fs.writeFileSync(filePath, content, 'utf-8');
635
459
 
636
460
  return this.getCommand(name, scope, projectPath, namespace);
637
461
  }
@@ -644,27 +468,29 @@ class CommandsService {
644
468
  ? this.userCommandsDir
645
469
  : this.getProjectCommandsDir(projectPath);
646
470
 
647
- const relativePath = normalizeCommandRelativePath(namespace
471
+ const relativePath = namespace
648
472
  ? path.join(namespace, `${name}.md`)
649
- : `${name}.md`);
473
+ : `${name}.md`;
474
+
650
475
  const filePath = path.join(baseDir, relativePath);
651
- const managedPath = scope === 'user' ? this.getManagedCommandPath(relativePath) : '';
652
- let removed = false;
653
476
 
654
- if (fs.existsSync(filePath)) {
655
- fs.unlinkSync(filePath);
656
- this.removeEmptyCommandDirs(baseDir, relativePath);
657
- removed = true;
477
+ if (!fs.existsSync(filePath)) {
478
+ return { success: false, message: '命令不存在' };
658
479
  }
659
480
 
660
- if (managedPath && fs.existsSync(managedPath)) {
661
- fs.unlinkSync(managedPath);
662
- this.removeEmptyCommandDirs(this.storageDir, relativePath);
663
- removed = true;
664
- }
481
+ fs.unlinkSync(filePath);
665
482
 
666
- if (!removed) {
667
- return { success: false, message: '命令不存在' };
483
+ // 如果目录为空,删除目录
484
+ if (namespace) {
485
+ const namespaceDir = path.join(baseDir, namespace);
486
+ try {
487
+ const remaining = fs.readdirSync(namespaceDir);
488
+ if (remaining.length === 0) {
489
+ fs.rmdirSync(namespaceDir);
490
+ }
491
+ } catch (err) {
492
+ // 忽略删除目录错误
493
+ }
668
494
  }
669
495
 
670
496
  return { success: true, message: '命令已删除' };
@@ -731,37 +557,11 @@ class CommandsService {
731
557
  return this.repoScanner.installCommand(command);
732
558
  }
733
559
 
734
- installLocalCommand(relativePath) {
735
- const normalizedPath = normalizeCommandRelativePath(relativePath);
736
- const managedPath = this.getManagedCommandPath(normalizedPath);
737
- const targetPath = path.join(this.userCommandsDir, normalizedPath);
738
-
739
- if (!fs.existsSync(managedPath)) {
740
- throw new Error(`本地命令 "${normalizedPath}" 不存在`);
741
- }
742
-
743
- if (fs.existsSync(targetPath)) {
744
- return { success: true, message: 'Already installed' };
745
- }
746
-
747
- this.copyFileRecursive(managedPath, targetPath);
748
- return { success: true, message: 'Installed successfully' };
749
- }
750
-
751
560
  /**
752
561
  * 卸载命令
753
562
  */
754
563
  uninstallCommand(relativePath) {
755
- const normalizedPath = normalizeCommandRelativePath(relativePath);
756
- const targetPath = path.join(this.userCommandsDir, normalizedPath);
757
-
758
- if (fs.existsSync(targetPath)) {
759
- fs.unlinkSync(targetPath);
760
- this.removeEmptyCommandDirs(this.userCommandsDir, normalizedPath);
761
- return { success: true, message: 'Uninstalled successfully' };
762
- }
763
-
764
- return { success: true, message: 'Not installed' };
564
+ return this.repoScanner.uninstall(relativePath);
765
565
  }
766
566
 
767
567
  // ==================== 格式转换 ====================
@@ -14,7 +14,7 @@ const opencodeChannelsService = require('./opencode-channels');
14
14
  const { AgentsService } = require('./agents-service');
15
15
  const { CommandsService } = require('./commands-service');
16
16
  const { SkillService } = require('./skill-service');
17
- const { PATHS, NATIVE_PATHS, getNativePathDir } = require('../../config/paths');
17
+ const { PATHS, NATIVE_PATHS } = require('../../config/paths');
18
18
 
19
19
  const CONFIG_VERSION = '1.4.0';
20
20
  const SKILL_FILE_ENCODING = 'base64';
@@ -25,8 +25,8 @@ const LEGACY_CC_TOOL_DIR = PATHS.base;
25
25
  const CLAUDE_SETTINGS_PATH = NATIVE_PATHS.claude.settings;
26
26
  const LEGACY_PLUGINS_DIR = path.join(LEGACY_CC_TOOL_DIR, 'plugins', 'installed');
27
27
  const LEGACY_PLUGINS_REGISTRY = path.join(LEGACY_CC_TOOL_DIR, 'plugins', 'registry.json');
28
- const CLAUDE_PLUGINS_DIR = NATIVE_PATHS.claude.plugins;
29
- const NATIVE_PLUGINS_REGISTRY = NATIVE_PATHS.claude.installedPlugins;
28
+ const CLAUDE_PLUGINS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'plugins');
29
+ const NATIVE_PLUGINS_REGISTRY = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
30
30
  const PLUGIN_IGNORE_DIRS = new Set(['.git', 'node_modules', '.DS_Store']);
31
31
  const PLUGIN_IGNORE_FILES = new Set(['.DS_Store']);
32
32
  const PLUGIN_SENSITIVE_PATTERNS = [
@@ -44,7 +44,7 @@ const CC_PROMPTS_PATH = PATHS.prompts;
44
44
  const CC_SECURITY_PATH = PATHS.security;
45
45
  const LEGACY_UI_CONFIG_PATH = PATHS.uiConfig;
46
46
  const LEGACY_NOTIFY_HOOK_PATH = PATHS.notifyHook;
47
- const GEMINI_SETTINGS_PATH = NATIVE_PATHS.gemini.settings;
47
+ const GEMINI_SETTINGS_PATH = path.join(path.dirname(NATIVE_PATHS.gemini.env), 'settings.json');
48
48
  const AGENT_PLATFORMS = ['claude', 'codex', 'opencode'];
49
49
  const COMMAND_PLATFORMS = ['claude', 'opencode'];
50
50
  const SKILL_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
@@ -156,7 +156,7 @@ function writeJsonFileAbsolute(filePath, data, overwrite, options = {}) {
156
156
  return 'failed';
157
157
  }
158
158
  if (fs.existsSync(filePath) && !overwrite) return 'skipped';
159
- ensureDir(getNativePathDir(filePath));
159
+ ensureDir(path.dirname(filePath));
160
160
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
161
161
  if (options.mode) {
162
162
  try {
@@ -173,7 +173,7 @@ function writeTextFileAbsolute(filePath, content, overwrite, options = {}) {
173
173
  return 'failed';
174
174
  }
175
175
  if (fs.existsSync(filePath) && !overwrite) return 'skipped';
176
- ensureDir(getNativePathDir(filePath));
176
+ ensureDir(path.dirname(filePath));
177
177
  fs.writeFileSync(filePath, content, 'utf8');
178
178
  if (options.mode) {
179
179
  try {
@@ -507,7 +507,7 @@ function syncImportedChannelsToNativeConfigs(importChannelsByType, nativeConfigs
507
507
  ]
508
508
  );
509
509
  if (persistedClaudeChannel?.id) {
510
- ensureDir(NATIVE_PATHS.claude.dir);
510
+ ensureDir(path.dirname(NATIVE_PATHS.claude.settings));
511
511
  channelsService.applyChannelToSettings(persistedClaudeChannel.id);
512
512
  }
513
513
  }
@@ -17,11 +17,12 @@ const REGISTRY_FILE = PATHS.configRegistry;
17
17
  const CONFIGS_DIR = PATHS.configs;
18
18
 
19
19
  // Claude Code native directories
20
+ const CLAUDE_HOME_DIR = path.dirname(NATIVE_PATHS.claude.settings);
20
21
  const CLAUDE_DIRS = {
21
- skills: NATIVE_PATHS.claude.skills,
22
- commands: NATIVE_PATHS.claude.commands,
23
- agents: NATIVE_PATHS.claude.agents,
24
- plugins: NATIVE_PATHS.claude.plugins
22
+ skills: path.join(CLAUDE_HOME_DIR, 'skills'),
23
+ commands: path.join(CLAUDE_HOME_DIR, 'commands'),
24
+ agents: path.join(CLAUDE_HOME_DIR, 'agents'),
25
+ plugins: path.join(CLAUDE_HOME_DIR, 'plugins')
25
26
  };
26
27
 
27
28
  // Valid config types
@@ -511,7 +512,7 @@ class ConfigRegistryService {
511
512
  this._importFileBasedConfigs(type, sourceDir, destDir, entryRelativePath, registry, result);
512
513
  } else if (entry.isFile() && entry.name.endsWith('.md')) {
513
514
  // This is a config file
514
- const name = entryRelativePath; // Key is the relative path
515
+ const name = normalizeRelativeConfigName(entryRelativePath);
515
516
 
516
517
  // Skip if already in registry
517
518
  if (registry[type][name]) {
@@ -520,8 +521,8 @@ class ConfigRegistryService {
520
521
  }
521
522
 
522
523
  // Copy file to cc-tool configs
523
- const sourcePath = path.join(sourceDir, entryRelativePath);
524
- const destPath = path.join(destDir, entryRelativePath);
524
+ const sourcePath = path.join(sourceDir, name);
525
+ const destPath = path.join(destDir, name);
525
526
 
526
527
  try {
527
528
  // Ensure destination directory exists
@@ -800,7 +801,7 @@ class ConfigRegistryService {
800
801
  if (entry.isDirectory()) {
801
802
  this._syncFileBasedRegistry(type, baseDir, entryRelativePath, registry, result);
802
803
  } else if (entry.isFile() && entry.name.endsWith('.md')) {
803
- const name = entryRelativePath;
804
+ const name = normalizeRelativeConfigName(entryRelativePath);
804
805
 
805
806
  if (!registry[type][name]) {
806
807
  registry[type][name] = {