autosnippet 2.1.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 (40) hide show
  1. package/README.md +189 -113
  2. package/bin/api-server.js +1 -4
  3. package/bin/cli.js +1 -50
  4. package/config/constitution.yaml +33 -107
  5. package/dashboard/dist/assets/{index-DbkbX1c-.js → index-B9py3ybr.js} +32 -32
  6. package/dashboard/dist/index.html +1 -1
  7. package/lib/bootstrap.js +5 -31
  8. package/lib/cli/SetupService.js +16 -14
  9. package/lib/core/capability/CapabilityProbe.js +8 -6
  10. package/lib/core/constitution/Constitution.js +13 -4
  11. package/lib/core/constitution/ConstitutionValidator.js +106 -211
  12. package/lib/core/gateway/Gateway.js +34 -98
  13. package/lib/core/gateway/GatewayActionRegistry.js +12 -1
  14. package/lib/core/permission/PermissionManager.js +2 -2
  15. package/lib/external/mcp/McpServer.js +4 -7
  16. package/lib/external/mcp/handlers/bootstrap.js +13 -1
  17. package/lib/external/mcp/handlers/browse.js +0 -7
  18. package/lib/external/mcp/handlers/candidate.js +1 -1
  19. package/lib/external/mcp/handlers/guard.js +11 -0
  20. package/lib/external/mcp/handlers/skill.js +186 -18
  21. package/lib/external/mcp/tools.js +40 -1
  22. package/lib/http/middleware/roleResolver.js +1 -1
  23. package/lib/http/routes/auth.js +2 -2
  24. package/lib/http/routes/monitoring.js +4 -4
  25. package/lib/http/routes/search.js +0 -17
  26. package/lib/injection/ServiceContainer.js +21 -40
  27. package/lib/service/candidate/CandidateService.js +12 -1
  28. package/lib/service/chat/ChatAgent.js +139 -18
  29. package/lib/service/chat/Memory.js +104 -0
  30. package/lib/service/chat/tools.js +244 -10
  31. package/lib/service/guard/GuardCheckEngine.js +9 -1
  32. package/lib/service/recipe/RecipeService.js +8 -0
  33. package/lib/service/skills/SkillHooks.js +126 -0
  34. package/package.json +1 -1
  35. package/scripts/init-db.js +1 -2
  36. package/templates/constitution.yaml +29 -85
  37. package/lib/core/session/SessionManager.js +0 -232
  38. package/lib/infrastructure/logging/ReasoningLogger.js +0 -269
  39. package/lib/infrastructure/monitoring/RoleDriftMonitor.js +0 -259
  40. package/lib/infrastructure/quality/ComplianceEvaluator.js +0 -326
@@ -5,12 +5,10 @@ import { InternalError } from '../../shared/errors/BaseError.js';
5
5
 
6
6
  /**
7
7
  * Gateway - 统一网关
8
- * 所有操作的唯一入口,负责:
9
- * 1. 权限检查
10
- * 2. 宪法验证
11
- * 3. 审计日志
12
- * 4. 会话管理
13
- * 5. 事件分发
8
+ * 所有操作的唯一入口。
9
+ *
10
+ * Pipeline (4 步):
11
+ * validate guard → route → audit
14
12
  */
15
13
  export class Gateway extends EventEmitter {
16
14
  constructor(config) {
@@ -18,14 +16,12 @@ export class Gateway extends EventEmitter {
18
16
  this.config = config;
19
17
  this.logger = Logger.getInstance();
20
18
  this.routes = new Map();
21
- this.plugins = [];
22
19
 
23
20
  // 依赖注入(稍后设置)
24
21
  this.constitution = null;
25
22
  this.constitutionValidator = null;
26
23
  this.permissionManager = null;
27
24
  this.auditLogger = null;
28
- this.sessionManager = null;
29
25
  }
30
26
 
31
27
  /**
@@ -36,13 +32,11 @@ export class Gateway extends EventEmitter {
36
32
  constitutionValidator,
37
33
  permissionManager,
38
34
  auditLogger,
39
- sessionManager,
40
35
  }) {
41
36
  this.constitution = constitution;
42
37
  this.constitutionValidator = constitutionValidator;
43
38
  this.permissionManager = permissionManager;
44
39
  this.auditLogger = auditLogger;
45
- this.sessionManager = sessionManager;
46
40
  }
47
41
 
48
42
  /**
@@ -63,14 +57,6 @@ export class Gateway extends EventEmitter {
63
57
  return [...this.routes.keys()];
64
58
  }
65
59
 
66
- /**
67
- * 注册插件
68
- */
69
- use(plugin) {
70
- this.plugins.push(plugin);
71
- this.logger.debug(`Plugin registered: ${plugin.name}`);
72
- }
73
-
74
60
  /**
75
61
  * 执行操作(主入口)
76
62
  */
@@ -95,28 +81,18 @@ export class Gateway extends EventEmitter {
95
81
  });
96
82
 
97
83
  try {
98
- // 1. 验证请求
84
+ // 1. validate — 请求格式
99
85
  this.validateRequest(request);
100
86
 
101
- // 2. 权限检查
102
- await this.checkPermission(context);
87
+ // 2. guard — 权限 + 宪法规则
88
+ await this.guard(context);
103
89
 
104
- // 3. 宪法验证
105
- await this.validateConstitution(context);
106
-
107
- // 4. 执行插件(pre-hook)
108
- await this.runPlugins('pre', context);
109
-
110
- // 5. 路由到处理器
90
+ // 3. route — 路由到处理器
111
91
  const result = await this.routeToHandler(context);
112
92
 
113
- // 6. 执行插件(post-hook)
114
- await this.runPlugins('post', context, result);
115
-
116
- // 7. 审计日志(成功)
93
+ // 4. audit — 记录成功
117
94
  await this.auditSuccess(context, result);
118
95
 
119
- // 8. 返回结果
120
96
  const duration = Date.now() - startTime;
121
97
  this.logger.info('Gateway: Request completed', {
122
98
  requestId,
@@ -130,7 +106,6 @@ export class Gateway extends EventEmitter {
130
106
  duration,
131
107
  };
132
108
  } catch (error) {
133
- // 审计日志(失败)
134
109
  await this.auditFailure(context, error);
135
110
 
136
111
  const duration = Date.now() - startTime;
@@ -155,7 +130,7 @@ export class Gateway extends EventEmitter {
155
130
 
156
131
  /**
157
132
  * 仅检查权限与宪法(不执行业务逻辑)
158
- * 用于 MCP Gateway gating — 只做 permission + constitution + audit,不路由到 handler
133
+ * 用于 MCP Gateway gating
159
134
  */
160
135
  async checkOnly(request) {
161
136
  const requestId = uuidv4();
@@ -173,13 +148,9 @@ export class Gateway extends EventEmitter {
173
148
 
174
149
  try {
175
150
  this.validateRequest(request);
176
- await this.checkPermission(context);
177
- await this.validateConstitution(context);
178
- await this.runPlugins('pre', context);
151
+ await this.guard(context);
179
152
 
180
- // 记录成功的 checkOnly 审计日志(供 MCP Gateway gating 审计追踪)
181
153
  await this.auditSuccess(context, { checkOnly: true });
182
-
183
154
  return { success: true, requestId };
184
155
  } catch (error) {
185
156
  await this.auditFailure(context, error);
@@ -195,8 +166,10 @@ export class Gateway extends EventEmitter {
195
166
  }
196
167
  }
197
168
 
169
+ // ─── Pipeline Steps ────────────────────────────────────
170
+
198
171
  /**
199
- * 验证请求格式
172
+ * validate — 验证请求格式
200
173
  */
201
174
  validateRequest(request) {
202
175
  if (!request.actor) {
@@ -208,38 +181,27 @@ export class Gateway extends EventEmitter {
208
181
  }
209
182
 
210
183
  /**
211
- * 权限检查
184
+ * guard — 权限检查 + 宪法验证
212
185
  */
213
- async checkPermission(context) {
214
- if (!this.permissionManager) {
215
- this.logger.warn('PermissionManager not set, skipping permission check');
216
- return;
186
+ async guard(context) {
187
+ // 权限检查
188
+ if (this.permissionManager) {
189
+ this.permissionManager.enforce(context.actor, context.action, context.resource);
217
190
  }
218
191
 
219
- this.permissionManager.enforce(context.actor, context.action, context.resource);
220
- }
221
-
222
- /**
223
- * 宪法验证
224
- */
225
- async validateConstitution(context) {
226
- if (!this.constitutionValidator) {
227
- this.logger.warn('ConstitutionValidator not set, skipping validation');
228
- return;
192
+ // 宪法数据完整性规则
193
+ if (this.constitutionValidator) {
194
+ await this.constitutionValidator.enforce({
195
+ actor: context.actor,
196
+ action: context.action,
197
+ resource: context.resource,
198
+ data: context.data,
199
+ });
229
200
  }
230
-
231
- const request = {
232
- actor: context.actor,
233
- action: context.action,
234
- resource: context.resource,
235
- data: context.data,
236
- };
237
-
238
- await this.constitutionValidator.enforce(request);
239
201
  }
240
202
 
241
203
  /**
242
- * 路由到处理器
204
+ * route — 路由到处理器
243
205
  */
244
206
  async routeToHandler(context) {
245
207
  const handler = this.routes.get(context.action);
@@ -252,23 +214,10 @@ export class Gateway extends EventEmitter {
252
214
  }
253
215
 
254
216
  /**
255
- * 执行插件
256
- */
257
- async runPlugins(phase, context, result = null) {
258
- for (const plugin of this.plugins) {
259
- if (plugin[phase]) {
260
- await plugin[phase](context, result);
261
- }
262
- }
263
- }
264
-
265
- /**
266
- * 审计成功
217
+ * audit — 记录成功
267
218
  */
268
219
  async auditSuccess(context, result) {
269
- if (!this.auditLogger) {
270
- return;
271
- }
220
+ if (!this.auditLogger) return;
272
221
 
273
222
  await this.auditLogger.log({
274
223
  requestId: context.requestId,
@@ -277,19 +226,15 @@ export class Gateway extends EventEmitter {
277
226
  resource: context.resource,
278
227
  result: 'success',
279
228
  duration: Date.now() - context.startTime,
280
- context: {
281
- session: context.session,
282
- },
229
+ context: { session: context.session },
283
230
  });
284
231
  }
285
232
 
286
233
  /**
287
- * 审计失败
234
+ * audit — 记录失败
288
235
  */
289
236
  async auditFailure(context, error) {
290
- if (!this.auditLogger) {
291
- return;
292
- }
237
+ if (!this.auditLogger) return;
293
238
 
294
239
  await this.auditLogger.log({
295
240
  requestId: context.requestId,
@@ -299,9 +244,7 @@ export class Gateway extends EventEmitter {
299
244
  result: 'failure',
300
245
  error: error.message,
301
246
  duration: Date.now() - context.startTime,
302
- context: {
303
- session: context.session,
304
- },
247
+ context: { session: context.session },
305
248
  });
306
249
  }
307
250
 
@@ -311,13 +254,6 @@ export class Gateway extends EventEmitter {
311
254
  getRoutes() {
312
255
  return Array.from(this.routes.keys());
313
256
  }
314
-
315
- /**
316
- * 获取所有插件
317
- */
318
- getPlugins() {
319
- return this.plugins.map((p) => p.name || 'anonymous');
320
- }
321
257
  }
322
258
 
323
259
  export default Gateway;
@@ -206,8 +206,19 @@ export function registerGatewayActions(gateway, container) {
206
206
 
207
207
  // ========== Search Actions ==========
208
208
 
209
+ // ========== Candidate Update (enrich/refine) ==========
210
+
211
+ gateway.register('candidate:update', async (ctx) => {
212
+ const service = container.get('candidateService');
213
+ return service.updateCandidate
214
+ ? service.updateCandidate(ctx.data.id, ctx.data, { userId: ctx.actor })
215
+ : service.createCandidate(ctx.data, { userId: ctx.actor });
216
+ });
217
+
218
+ // ========== Search ==========
219
+
209
220
  gateway.register('search:query', async (ctx) => {
210
- const service = container.get('searchService');
221
+ const service = container.get('searchEngine');
211
222
  return service.search(ctx.data.keyword, ctx.data.options);
212
223
  });
213
224
 
@@ -123,7 +123,7 @@ export class PermissionManager {
123
123
  * 处理多种格式:
124
124
  * - read_recipes -> read:recipes
125
125
  * - read:recipes -> read:recipes(已规范化)
126
- * - perm_cursor_agent_read_recipes -> read:recipes(测试使用的格式)
126
+ * - perm_external_agent_read_recipes -> read:recipes(测试使用的格式)
127
127
  */
128
128
  _normalizeAction(action) {
129
129
  // 如果已经包含冒号,直接返回
@@ -134,7 +134,7 @@ export class PermissionManager {
134
134
  // 处理测试格式:perm_actor_action_resource -> action:resource
135
135
  if (action.startsWith('perm_')) {
136
136
  const parts = action.split('_');
137
- // perm_cursor_agent_read_recipes -> ['perm', 'cursor', 'agent', 'read', 'recipes']
137
+ // perm_external_agent_read_recipes -> ['perm', 'cursor', 'agent', 'read', 'recipes']
138
138
  // 跳过 'perm' 和 actor 名称部分,从实际的 action 部分开始
139
139
  if (parts.length >= 4) {
140
140
  // 尝试找到 action 部分(常见的 action 包括 read, create, delete, submit, approve, reject)
@@ -61,10 +61,7 @@ export class McpServer {
61
61
  db: components.db,
62
62
  auditLogger: components.auditLogger,
63
63
  gateway: components.gateway,
64
- reasoningLogger: components.reasoningLogger,
65
- roleDriftMonitor: components.roleDriftMonitor,
66
- complianceEvaluator: components.complianceEvaluator,
67
- sessionManager: components.sessionManager,
64
+ constitution: components.constitution,
68
65
  projectRoot: process.env.ASD_PROJECT_DIR || process.cwd(),
69
66
  });
70
67
 
@@ -123,7 +120,6 @@ export class McpServer {
123
120
  case 'autosnippet_list_recipes': return browseHandlers.listRecipes(ctx, args);
124
121
  case 'autosnippet_get_recipe': return browseHandlers.getRecipe(ctx, args);
125
122
  case 'autosnippet_recipe_insights': return browseHandlers.recipeInsights(ctx, args);
126
- case 'autosnippet_compliance_report': return browseHandlers.complianceReport(ctx, args);
127
123
  case 'autosnippet_confirm_usage': return browseHandlers.confirmUsage(ctx, args);
128
124
  // 项目结构 & 图谱
129
125
  case 'autosnippet_get_targets': return structureHandlers.getTargets(ctx);
@@ -147,9 +143,10 @@ export class McpServer {
147
143
  // Bootstrap 冷启动
148
144
  case 'autosnippet_bootstrap_knowledge': return bootstrapHandlers.bootstrapKnowledge(ctx, args);
149
145
  case 'autosnippet_bootstrap_refine': return bootstrapHandlers.bootstrapRefine(ctx, args);
150
- // Skills 加载
146
+ // Skills 加载 & 创建
151
147
  case 'autosnippet_list_skills': return skillHandlers.listSkills();
152
148
  case 'autosnippet_load_skill': return skillHandlers.loadSkill(ctx, args);
149
+ case 'autosnippet_create_skill': return skillHandlers.createSkill(ctx, args);
153
150
  default: throw new Error(`Unknown tool: ${name}`);
154
151
  }
155
152
  }
@@ -167,7 +164,7 @@ export class McpServer {
167
164
  if (!gateway) return; // Gateway 未初始化,降级放行
168
165
 
169
166
  const result = await gateway.checkOnly({
170
- actor: 'cursor_agent',
167
+ actor: 'external_agent',
171
168
  action: mapping.action,
172
169
  resource: mapping.resource,
173
170
  data: args || {},
@@ -664,6 +664,18 @@ export async function bootstrapKnowledge(ctx, args) {
664
664
  responseData.skillsEnhanced = skillsEnhanced;
665
665
  responseData.message = `Bootstrap 完成: ${allFiles.length} files, ${allTargets.length} targets, ${depEdgesWritten} graph edges, ${candidateResults.created} 条单一职责候选已创建${skillsEnhanced ? '(Skill 增强)' : ''}。${skillContext?.loaded?.length ? `已加载 Skills: ${skillContext.loaded.join(', ')}。` : ''}⚠️ 候选为启发式初稿,请务必执行后续 AI 精炼步骤提升质量。`;
666
666
 
667
+ // ── SkillHooks: onBootstrapComplete (fire-and-forget) ──
668
+ try {
669
+ const skillHooks = ctx.container.get('skillHooks');
670
+ skillHooks.run('onBootstrapComplete', {
671
+ filesScanned: allFiles.length,
672
+ targetsFound: allTargets.length,
673
+ candidatesCreated: candidateResults.created,
674
+ candidatesFailed: candidateResults.failed,
675
+ }, { projectRoot: ctx.container.get('database')?.filename || '' })
676
+ .catch(() => {}); // fire-and-forget
677
+ } catch { /* skillHooks not available */ }
678
+
667
679
  return envelope({
668
680
  success: true,
669
681
  data: responseData,
@@ -695,7 +707,7 @@ export async function bootstrapRefine(ctx, args) {
695
707
  const result = await candidateService.refineBootstrapCandidates(
696
708
  aiProvider,
697
709
  { candidateIds: args.candidateIds, userPrompt: args.userPrompt, dryRun: args.dryRun },
698
- { userId: 'cursor_agent' },
710
+ { userId: 'external_agent' },
699
711
  );
700
712
 
701
713
  return envelope({
@@ -119,13 +119,6 @@ export async function recipeInsights(ctx, args) {
119
119
  return envelope({ success: true, data: insights, meta: { tool: 'autosnippet_recipe_insights' } });
120
120
  }
121
121
 
122
- export async function complianceReport(ctx, args = {}) {
123
- const evaluator = ctx.container.get('complianceEvaluator');
124
- if (!evaluator) return envelope({ success: false, message: 'ComplianceEvaluator not available', meta: { tool: 'autosnippet_compliance_report' } });
125
- const report = await evaluator.evaluate({ period: args.period || 'all' });
126
- return envelope({ success: true, data: report, meta: { tool: 'autosnippet_compliance_report' } });
127
- }
128
-
129
122
  export async function confirmUsage(ctx, args) {
130
123
  if (!args.recipeId) throw new Error('recipeId is required');
131
124
  const recipeService = ctx.container.get('recipeService');
@@ -65,7 +65,7 @@ export function buildCandidateMetadata(obj) {
65
65
  * 保留此函数作为 MCP handler 层的快捷入口,保持向后兼容。
66
66
  */
67
67
  async function _createCandidateItem(candidateService, item, source, extraMeta = {}) {
68
- return candidateService.createFromToolParams(item, source, extraMeta, { userId: 'cursor_agent' });
68
+ return candidateService.createFromToolParams(item, source, extraMeta, { userId: 'external_agent' });
69
69
  }
70
70
 
71
71
  // ─── 限流检查 ──────────────────────────────────────────────
@@ -24,6 +24,17 @@ export async function guardCheck(ctx, args) {
24
24
  const language = args.language || detectLanguage(args.filePath || '');
25
25
  const violations = engine.checkCode(args.code, language);
26
26
 
27
+ // ── SkillHooks: onGuardCheck — 允许 hooks 修改 violations ──
28
+ try {
29
+ const skillHooks = ctx.container.get('skillHooks');
30
+ if (skillHooks.has('onGuardCheck')) {
31
+ for (let i = 0; i < violations.length; i++) {
32
+ const modified = await skillHooks.run('onGuardCheck', violations[i], { language });
33
+ if (modified && typeof modified === 'object') violations[i] = modified;
34
+ }
35
+ }
36
+ } catch { /* skillHooks not available */ }
37
+
27
38
  const warnings = [];
28
39
  if (language === 'unknown') {
29
40
  warnings.push('未能识别语言,部分语言相关规则可能未执行。建议提供 language 或 filePath 参数。');
@@ -15,7 +15,9 @@ import path from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
16
 
17
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
- const SKILLS_DIR = path.resolve(__dirname, '../../../../skills');
18
+ const PROJECT_ROOT = path.resolve(__dirname, '../../../..');
19
+ const SKILLS_DIR = path.resolve(PROJECT_ROOT, 'skills');
20
+ const PROJECT_SKILLS_DIR = path.resolve(PROJECT_ROOT, '.autosnippet', 'skills');
19
21
 
20
22
  /**
21
23
  * Skill 名称 → 摘要描述映射(用于 list_skills 返回)
@@ -23,10 +25,10 @@ const SKILLS_DIR = path.resolve(__dirname, '../../../../skills');
23
25
  * 从 SKILL.md 的 frontmatter description 提取。
24
26
  * 如果解析失败,返回 Skill 名称本身。
25
27
  */
26
- function _parseSkillSummary(skillName) {
28
+ function _parseSkillSummary(skillName, baseDir = SKILLS_DIR) {
27
29
  try {
28
30
  const content = fs.readFileSync(
29
- path.join(SKILLS_DIR, skillName, 'SKILL.md'), 'utf8',
31
+ path.join(baseDir, skillName, 'SKILL.md'), 'utf8',
30
32
  );
31
33
  // 提取 frontmatter 的 description 字段
32
34
  const descMatch = content.match(/^description:\s*(.+?)(?:\n|$)/m);
@@ -72,16 +74,25 @@ const SKILL_USE_CASES = {
72
74
  */
73
75
  export function listSkills() {
74
76
  try {
75
- const dirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
76
- .filter(d => d.isDirectory())
77
- .map(d => d.name)
78
- .sort();
77
+ const skillMap = new Map();
79
78
 
80
- const skills = dirs.map(name => ({
81
- name,
82
- summary: _parseSkillSummary(name),
83
- useCase: SKILL_USE_CASES[name] || null,
84
- }));
79
+ // 内置 Skills
80
+ const builtinDirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
81
+ .filter(d => d.isDirectory()).map(d => d.name);
82
+ for (const name of builtinDirs) {
83
+ skillMap.set(name, { name, source: 'builtin', summary: _parseSkillSummary(name, SKILLS_DIR), useCase: SKILL_USE_CASES[name] || null });
84
+ }
85
+
86
+ // 项目级 Skills(覆盖同名内置)
87
+ try {
88
+ const projectDirs = fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true })
89
+ .filter(d => d.isDirectory()).map(d => d.name);
90
+ for (const name of projectDirs) {
91
+ skillMap.set(name, { name, source: 'project', summary: _parseSkillSummary(name, PROJECT_SKILLS_DIR), useCase: SKILL_USE_CASES[name] || null });
92
+ }
93
+ } catch { /* no project skills */ }
94
+
95
+ const skills = [...skillMap.values()].sort((a, b) => a.name.localeCompare(b.name));
85
96
 
86
97
  return JSON.stringify({
87
98
  success: true,
@@ -120,7 +131,11 @@ export function loadSkill(_ctx, args) {
120
131
  });
121
132
  }
122
133
 
123
- const skillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
134
+ // 项目级 Skills 优先
135
+ const projectSkillPath = path.join(PROJECT_SKILLS_DIR, skillName, 'SKILL.md');
136
+ const builtinSkillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
137
+ const skillPath = fs.existsSync(projectSkillPath) ? projectSkillPath : builtinSkillPath;
138
+ const source = skillPath === projectSkillPath ? 'project' : 'builtin';
124
139
 
125
140
  try {
126
141
  let content = fs.readFileSync(skillPath, 'utf8');
@@ -141,6 +156,7 @@ export function loadSkill(_ctx, args) {
141
156
  success: true,
142
157
  data: {
143
158
  skillName,
159
+ source,
144
160
  content,
145
161
  charCount: content.length,
146
162
  useCase: SKILL_USE_CASES[skillName] || null,
@@ -148,22 +164,174 @@ export function loadSkill(_ctx, args) {
148
164
  },
149
165
  });
150
166
  } catch {
151
- // 列出可用 Skills 帮助 Agent 修正
152
- const available = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
153
- .filter(d => d.isDirectory())
154
- .map(d => d.name);
167
+ // 列出所有可用 Skills
168
+ const available = new Set();
169
+ try { fs.readdirSync(SKILLS_DIR, { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
170
+ try { fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
155
171
 
156
172
  return JSON.stringify({
157
173
  success: false,
158
174
  error: {
159
175
  code: 'SKILL_NOT_FOUND',
160
176
  message: `Skill "${skillName}" not found`,
161
- availableSkills: available,
177
+ availableSkills: [...available],
162
178
  },
163
179
  });
164
180
  }
165
181
  }
166
182
 
183
+ // ═══════════════════════════════════════════════════════════
184
+ // Handler: createSkill
185
+ // ═══════════════════════════════════════════════════════════
186
+
187
+ /**
188
+ * 创建项目级 Skill — 写入 .autosnippet/skills/<name>/SKILL.md
189
+ * 创建后自动 regenerate 编辑器索引(.cursor/rules/autosnippet-skills.mdc)
190
+ *
191
+ * @param {object} _ctx MCP context
192
+ * @param {object} args { name, description, content, overwrite? }
193
+ * @returns {string} JSON envelope
194
+ */
195
+ export function createSkill(_ctx, args) {
196
+ const { name, description, content, overwrite = false } = args || {};
197
+
198
+ // ── 参数校验 ──
199
+ if (!name || !description || !content) {
200
+ return JSON.stringify({
201
+ success: false,
202
+ error: { code: 'MISSING_PARAM', message: 'name, description, content are all required' },
203
+ });
204
+ }
205
+
206
+ // 名称格式校验:kebab-case(允许字母、数字、连字符)
207
+ if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(name) || name.length < 3 || name.length > 64) {
208
+ return JSON.stringify({
209
+ success: false,
210
+ error: {
211
+ code: 'INVALID_NAME',
212
+ message: `Skill name must be kebab-case (a-z, 0-9, -), 3-64 chars. Got: "${name}"`,
213
+ },
214
+ });
215
+ }
216
+
217
+ // 不允许覆盖内置 Skill
218
+ const builtinSkillPath = path.join(SKILLS_DIR, name, 'SKILL.md');
219
+ if (fs.existsSync(builtinSkillPath)) {
220
+ return JSON.stringify({
221
+ success: false,
222
+ error: {
223
+ code: 'BUILTIN_CONFLICT',
224
+ message: `"${name}" is a built-in Skill and cannot be overwritten. Choose a different name.`,
225
+ },
226
+ });
227
+ }
228
+
229
+ // 检查同名项目级 Skill
230
+ const skillDir = path.join(PROJECT_SKILLS_DIR, name);
231
+ const skillPath = path.join(skillDir, 'SKILL.md');
232
+ if (fs.existsSync(skillPath) && !overwrite) {
233
+ return JSON.stringify({
234
+ success: false,
235
+ error: {
236
+ code: 'ALREADY_EXISTS',
237
+ message: `Project skill "${name}" already exists. Set overwrite=true to replace.`,
238
+ },
239
+ });
240
+ }
241
+
242
+ // ── 写入 SKILL.md ──
243
+ try {
244
+ fs.mkdirSync(skillDir, { recursive: true });
245
+
246
+ const frontmatter = [
247
+ '---',
248
+ `name: ${name}`,
249
+ `description: ${description}`,
250
+ '---',
251
+ '',
252
+ ].join('\n');
253
+
254
+ fs.writeFileSync(skillPath, frontmatter + content, 'utf8');
255
+ } catch (err) {
256
+ return JSON.stringify({
257
+ success: false,
258
+ error: { code: 'WRITE_ERROR', message: `Failed to write SKILL.md: ${err.message}` },
259
+ });
260
+ }
261
+
262
+ // ── regenerate 编辑器索引 ──
263
+ const indexResult = _regenerateEditorIndex();
264
+
265
+ return JSON.stringify({
266
+ success: true,
267
+ data: {
268
+ skillName: name,
269
+ path: skillPath,
270
+ overwritten: fs.existsSync(skillPath) && overwrite,
271
+ editorIndex: indexResult,
272
+ hint: `Skill "${name}" created. Use autosnippet_load_skill to verify content.`,
273
+ },
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Regenerate .cursor/rules/autosnippet-skills.mdc 索引文件
279
+ * 扫描所有项目级 Skills,生成摘要索引供 External Agent 被动发现
280
+ *
281
+ * @returns {{ success: boolean, path?: string, skillCount?: number, error?: string }}
282
+ */
283
+ function _regenerateEditorIndex() {
284
+ try {
285
+ // 扫描项目级 Skills
286
+ let projectSkills = [];
287
+ try {
288
+ const dirs = fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true })
289
+ .filter(d => d.isDirectory())
290
+ .map(d => d.name);
291
+ for (const name of dirs) {
292
+ const summary = _parseSkillSummary(name, PROJECT_SKILLS_DIR);
293
+ projectSkills.push({ name, summary });
294
+ }
295
+ } catch { /* no project skills dir */ }
296
+
297
+ if (projectSkills.length === 0) {
298
+ // 没有项目级 Skills 时,删除索引文件(如果存在)
299
+ const indexPath = path.join(PROJECT_ROOT, '.cursor', 'rules', 'autosnippet-skills.mdc');
300
+ try { fs.unlinkSync(indexPath); } catch { /* not exists */ }
301
+ return { success: true, skillCount: 0 };
302
+ }
303
+
304
+ // 生成 .mdc 内容
305
+ const skillLines = projectSkills
306
+ .map(s => `- **${s.name}**: ${s.summary}`)
307
+ .join('\n');
308
+
309
+ const mdcContent = [
310
+ '---',
311
+ 'description: AutoSnippet 项目级 Skills 索引(自动生成,请勿手动编辑)',
312
+ 'alwaysApply: true',
313
+ '---',
314
+ '',
315
+ '# AutoSnippet Project Skills',
316
+ '',
317
+ `本项目已注册 ${projectSkills.length} 个自定义 Skill。使用 \`autosnippet_load_skill\` 工具加载完整内容。`,
318
+ '',
319
+ skillLines,
320
+ '',
321
+ ].join('\n');
322
+
323
+ // 写入 .cursor/rules/
324
+ const rulesDir = path.join(PROJECT_ROOT, '.cursor', 'rules');
325
+ fs.mkdirSync(rulesDir, { recursive: true });
326
+ const indexPath = path.join(rulesDir, 'autosnippet-skills.mdc');
327
+ fs.writeFileSync(indexPath, mdcContent, 'utf8');
328
+
329
+ return { success: true, path: indexPath, skillCount: projectSkills.length };
330
+ } catch (err) {
331
+ return { success: false, error: err.message };
332
+ }
333
+ }
334
+
167
335
  /**
168
336
  * 推荐相关 Skills(基于静态映射)
169
337
  */