autosnippet 2.0.2 → 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.
- package/README.md +189 -113
- package/bin/api-server.js +1 -4
- package/bin/cli.js +1 -50
- package/config/constitution.yaml +33 -107
- package/dashboard/dist/assets/{icons-B4FfLfBA.js → icons-B5rs8uNb.js} +85 -80
- package/dashboard/dist/assets/index-0YzLw2ga.css +1 -0
- package/dashboard/dist/assets/index-B9py3ybr.js +154 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/bootstrap.js +5 -31
- package/lib/cli/SetupService.js +16 -14
- package/lib/core/capability/CapabilityProbe.js +8 -6
- package/lib/core/constitution/Constitution.js +13 -4
- package/lib/core/constitution/ConstitutionValidator.js +106 -211
- package/lib/core/gateway/Gateway.js +34 -98
- package/lib/core/gateway/GatewayActionRegistry.js +12 -1
- package/lib/core/permission/PermissionManager.js +2 -2
- package/lib/external/mcp/McpServer.js +4 -7
- package/lib/external/mcp/handlers/bootstrap.js +13 -1
- package/lib/external/mcp/handlers/browse.js +0 -7
- package/lib/external/mcp/handlers/candidate.js +1 -1
- package/lib/external/mcp/handlers/guard.js +11 -0
- package/lib/external/mcp/handlers/skill.js +186 -18
- package/lib/external/mcp/tools.js +40 -1
- package/lib/http/middleware/roleResolver.js +1 -1
- package/lib/http/routes/auth.js +2 -2
- package/lib/http/routes/commands.js +58 -3
- package/lib/http/routes/monitoring.js +4 -4
- package/lib/http/routes/recipes.js +96 -4
- package/lib/http/routes/search.js +34 -35
- package/lib/injection/ServiceContainer.js +21 -40
- package/lib/service/candidate/CandidateService.js +12 -1
- package/lib/service/chat/ChatAgent.js +171 -30
- package/lib/service/chat/Memory.js +104 -0
- package/lib/service/chat/tools.js +244 -10
- package/lib/service/guard/GuardCheckEngine.js +9 -1
- package/lib/service/knowledge/KnowledgeGraphService.js +20 -9
- package/lib/service/recipe/RecipeService.js +8 -0
- package/lib/service/skills/SkillHooks.js +126 -0
- package/package.json +1 -1
- package/scripts/init-db.js +1 -2
- package/templates/constitution.yaml +29 -85
- package/dashboard/dist/assets/index-ChxJxX4B.js +0 -154
- package/dashboard/dist/assets/index-DwAp1mx5.css +0 -1
- package/lib/core/session/SessionManager.js +0 -232
- package/lib/infrastructure/logging/ReasoningLogger.js +0 -269
- package/lib/infrastructure/monitoring/RoleDriftMonitor.js +0 -259
- 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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
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
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
186
|
+
async guard(context) {
|
|
187
|
+
// 权限检查
|
|
188
|
+
if (this.permissionManager) {
|
|
189
|
+
this.permissionManager.enforce(context.actor, context.action, context.resource);
|
|
217
190
|
}
|
|
218
191
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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('
|
|
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
|
-
* -
|
|
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
|
-
//
|
|
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
|
-
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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(
|
|
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
|
|
76
|
-
.filter(d => d.isDirectory())
|
|
77
|
-
.map(d => d.name)
|
|
78
|
-
.sort();
|
|
77
|
+
const skillMap = new Map();
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
152
|
-
const available =
|
|
153
|
-
|
|
154
|
-
|
|
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
|
*/
|