autosnippet 2.7.1 → 2.8.2

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.
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>AutoSnippet Dashboard</title>
8
- <script type="module" crossorigin src="/assets/index-BjfUm8p9.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-Duc8Qk-c.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/yaml-qRaU8Ldn.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-BotF760a.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/axios-C0Zqfgkc.js">
@@ -54,6 +54,12 @@ export class McpServer {
54
54
 
55
55
  // 路径安全守卫 — 在任何写操作前配置
56
56
  const projectRoot = process.env.ASD_PROJECT_DIR || process.cwd();
57
+
58
+ // 切换工作目录到项目根 — 确保 DB 等相对路径正确解析
59
+ if (projectRoot !== process.cwd()) {
60
+ process.chdir(projectRoot);
61
+ }
62
+
57
63
  Bootstrap.configurePathGuard(projectRoot);
58
64
 
59
65
  this.bootstrap = new Bootstrap();
@@ -148,10 +154,12 @@ export class McpServer {
148
154
  // Bootstrap 冷启动
149
155
  case 'autosnippet_bootstrap_knowledge': return bootstrapHandlers.bootstrapKnowledge(ctx, args);
150
156
  case 'autosnippet_bootstrap_refine': return bootstrapHandlers.bootstrapRefine(ctx, args);
151
- // Skills 加载 & 创建 & 推荐
157
+ // Skills 加载 & 创建 & 管理 & 推荐
152
158
  case 'autosnippet_list_skills': return skillHandlers.listSkills();
153
159
  case 'autosnippet_load_skill': return skillHandlers.loadSkill(ctx, args);
154
160
  case 'autosnippet_create_skill': return skillHandlers.createSkill(ctx, args);
161
+ case 'autosnippet_delete_skill': return skillHandlers.deleteSkill(ctx, args);
162
+ case 'autosnippet_update_skill': return skillHandlers.updateSkill(ctx, args);
155
163
  case 'autosnippet_suggest_skills': return skillHandlers.suggestSkills(ctx);
156
164
  default: throw new Error(`Unknown tool: ${name}`);
157
165
  }
@@ -200,9 +208,9 @@ export class McpServer {
200
208
  await this.initialize();
201
209
  const transport = new StdioServerTransport();
202
210
  await this.server.connect(transport);
203
- this.logger.info('MCP Server started (stdio) — 31 tools');
211
+ this.logger.info('MCP Server started (stdio) — 38 tools');
204
212
  // 在 stderr 写一行简洁的就绪通知(不使用 winston,仅用于 Cursor 日志面板 & 调试)
205
- process.stderr.write('AutoSnippet MCP ready — 31 tools\n');
213
+ process.stderr.write('AutoSnippet MCP ready — 38 tools\n');
206
214
  }
207
215
 
208
216
  async shutdown() {
@@ -399,6 +399,204 @@ function _regenerateEditorIndex() {
399
399
  }
400
400
  }
401
401
 
402
+ // ═══════════════════════════════════════════════════════════
403
+ // Handler: deleteSkill
404
+ // ═══════════════════════════════════════════════════════════
405
+
406
+ /**
407
+ * 删除项目级 Skill — 移除 {projectRoot}/AutoSnippet/skills/<name>/ 整个目录
408
+ * 内置 Skill 不可删除。删除后自动 regenerate 编辑器索引。
409
+ *
410
+ * @param {object} _ctx MCP context
411
+ * @param {object} args { name: string }
412
+ * @returns {string} JSON envelope
413
+ */
414
+ export function deleteSkill(_ctx, args) {
415
+ const { name } = args || {};
416
+
417
+ if (!name) {
418
+ return JSON.stringify({
419
+ success: false,
420
+ error: { code: 'MISSING_PARAM', message: 'name is required' },
421
+ });
422
+ }
423
+
424
+ // 不允许删除内置 Skill
425
+ const builtinSkillPath = path.join(SKILLS_DIR, name);
426
+ if (fs.existsSync(builtinSkillPath)) {
427
+ return JSON.stringify({
428
+ success: false,
429
+ error: {
430
+ code: 'BUILTIN_PROTECTED',
431
+ message: `"${name}" is a built-in Skill and cannot be deleted.`,
432
+ },
433
+ });
434
+ }
435
+
436
+ // 检查项目级 Skill 是否存在
437
+ const projectSkillsDir = _getProjectSkillsDir();
438
+ const skillDir = path.join(projectSkillsDir, name);
439
+ if (!fs.existsSync(skillDir)) {
440
+ return JSON.stringify({
441
+ success: false,
442
+ error: {
443
+ code: 'SKILL_NOT_FOUND',
444
+ message: `Project skill "${name}" not found.`,
445
+ },
446
+ });
447
+ }
448
+
449
+ // ── 路径安全检查 ──
450
+ try {
451
+ pathGuard.assertProjectWriteSafe(skillDir);
452
+ } catch (err) {
453
+ return JSON.stringify({
454
+ success: false,
455
+ error: { code: 'PATH_GUARD', message: err.message },
456
+ });
457
+ }
458
+
459
+ // ── 删除目录 ──
460
+ try {
461
+ fs.rmSync(skillDir, { recursive: true, force: true });
462
+ } catch (err) {
463
+ return JSON.stringify({
464
+ success: false,
465
+ error: { code: 'DELETE_ERROR', message: `Failed to delete skill: ${err.message}` },
466
+ });
467
+ }
468
+
469
+ // ── regenerate 编辑器索引 ──
470
+ const indexResult = _regenerateEditorIndex();
471
+
472
+ return JSON.stringify({
473
+ success: true,
474
+ data: {
475
+ skillName: name,
476
+ deleted: true,
477
+ editorIndex: indexResult,
478
+ hint: `Skill "${name}" deleted successfully.`,
479
+ },
480
+ });
481
+ }
482
+
483
+ // ═══════════════════════════════════════════════════════════
484
+ // Handler: updateSkill
485
+ // ═══════════════════════════════════════════════════════════
486
+
487
+ /**
488
+ * 更新项目级 Skill — 修改 description 和/或 content
489
+ * 内置 Skill 不可更新。更新后自动 regenerate 编辑器索引。
490
+ *
491
+ * @param {object} _ctx MCP context
492
+ * @param {object} args { name, description?, content? }
493
+ * @returns {string} JSON envelope
494
+ */
495
+ export function updateSkill(_ctx, args) {
496
+ const { name, description, content } = args || {};
497
+
498
+ if (!name) {
499
+ return JSON.stringify({
500
+ success: false,
501
+ error: { code: 'MISSING_PARAM', message: 'name is required' },
502
+ });
503
+ }
504
+
505
+ if (!description && !content) {
506
+ return JSON.stringify({
507
+ success: false,
508
+ error: { code: 'NOTHING_TO_UPDATE', message: 'At least one of description or content must be provided.' },
509
+ });
510
+ }
511
+
512
+ // 不允许更新内置 Skill
513
+ const builtinSkillPath = path.join(SKILLS_DIR, name, 'SKILL.md');
514
+ if (fs.existsSync(builtinSkillPath)) {
515
+ return JSON.stringify({
516
+ success: false,
517
+ error: {
518
+ code: 'BUILTIN_PROTECTED',
519
+ message: `"${name}" is a built-in Skill and cannot be updated. Fork it as a project skill instead.`,
520
+ },
521
+ });
522
+ }
523
+
524
+ // 检查项目级 Skill 是否存在
525
+ const projectSkillsDir = _getProjectSkillsDir();
526
+ const skillPath = path.join(projectSkillsDir, name, 'SKILL.md');
527
+ if (!fs.existsSync(skillPath)) {
528
+ return JSON.stringify({
529
+ success: false,
530
+ error: {
531
+ code: 'SKILL_NOT_FOUND',
532
+ message: `Project skill "${name}" not found. Use autosnippet_create_skill to create it first.`,
533
+ },
534
+ });
535
+ }
536
+
537
+ try {
538
+ // ── 读取现有文件 ──
539
+ const existing = fs.readFileSync(skillPath, 'utf8');
540
+
541
+ // 解析现有 frontmatter
542
+ const fmMatch = existing.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
543
+ let oldFm = '';
544
+ let oldBody = existing;
545
+ if (fmMatch) {
546
+ oldFm = fmMatch[1];
547
+ oldBody = fmMatch[2];
548
+ }
549
+
550
+ // 解析已有字段
551
+ const getField = (fm, key) => {
552
+ const m = fm.match(new RegExp(`^${key}:\\s*(.+?)$`, 'm'));
553
+ return m ? m[1].trim() : null;
554
+ };
555
+
556
+ const newDesc = description || getField(oldFm, 'description') || name;
557
+ const newBody = content !== undefined && content !== null ? content : oldBody;
558
+
559
+ // 保留原有字段
560
+ const createdBy = getField(oldFm, 'createdBy') || 'external-ai';
561
+ const createdAt = getField(oldFm, 'createdAt') || new Date().toISOString();
562
+ const title = getField(oldFm, 'title');
563
+
564
+ // 重建 frontmatter
565
+ const fmLines = ['---', `name: ${name}`];
566
+ if (title) fmLines.push(`title: ${title}`);
567
+ fmLines.push(
568
+ `description: ${newDesc}`,
569
+ `createdBy: ${createdBy}`,
570
+ `createdAt: ${createdAt}`,
571
+ `updatedAt: ${new Date().toISOString()}`,
572
+ '---',
573
+ '',
574
+ );
575
+
576
+ pathGuard.assertProjectWriteSafe(path.join(projectSkillsDir, name));
577
+ fs.writeFileSync(skillPath, fmLines.join('\n') + newBody, 'utf8');
578
+ } catch (err) {
579
+ return JSON.stringify({
580
+ success: false,
581
+ error: { code: 'UPDATE_ERROR', message: `Failed to update skill: ${err.message}` },
582
+ });
583
+ }
584
+
585
+ // ── regenerate 编辑器索引 ──
586
+ const indexResult = _regenerateEditorIndex();
587
+
588
+ return JSON.stringify({
589
+ success: true,
590
+ data: {
591
+ skillName: name,
592
+ updated: true,
593
+ fieldsUpdated: [description ? 'description' : null, content ? 'content' : null].filter(Boolean),
594
+ editorIndex: indexResult,
595
+ hint: `Skill "${name}" updated. Use autosnippet_load_skill to verify content.`,
596
+ },
597
+ });
598
+ }
599
+
402
600
  // ═══════════════════════════════════════════════════════════
403
601
  // Handler: suggestSkills
404
602
  // ═══════════════════════════════════════════════════════════
@@ -1,5 +1,5 @@
1
1
  /**
2
- * MCP 工具定义(34 个)+ Gateway 映射
2
+ * MCP 工具定义(38 个)+ Gateway 映射
3
3
  *
4
4
  * 只包含 JSON Schema 级别的声明,不含任何业务逻辑。
5
5
  */
@@ -18,6 +18,8 @@ export const TOOL_GATEWAY_MAP = {
18
18
  autosnippet_bootstrap_knowledge: { action: 'knowledge:bootstrap', resource: 'knowledge' },
19
19
  autosnippet_bootstrap_refine: { action: 'candidate:update', resource: 'candidates' },
20
20
  autosnippet_create_skill: { action: 'create:skills', resource: 'skills' },
21
+ autosnippet_delete_skill: { action: 'delete:skills', resource: 'skills' },
22
+ autosnippet_update_skill: { action: 'update:skills', resource: 'skills' },
21
23
  };
22
24
 
23
25
  export const TOOLS = [
@@ -684,6 +686,57 @@ export const TOOLS = [
684
686
  ' • 候选被大量驳回时',
685
687
  inputSchema: { type: 'object', properties: {}, required: [] },
686
688
  },
689
+ // 37. 删除项目级 Skill
690
+ {
691
+ name: 'autosnippet_delete_skill',
692
+ description:
693
+ '删除一个项目级 Skill 及其目录。\n' +
694
+ '⚠️ 内置 Skill 不可删除。删除后自动更新编辑器索引。\n' +
695
+ '\n' +
696
+ '使用场景:\n' +
697
+ ' • 清理不再需要的自定义 Skill\n' +
698
+ ' • 移除过时或错误的操作指南',
699
+ inputSchema: {
700
+ type: 'object',
701
+ properties: {
702
+ name: {
703
+ type: 'string',
704
+ description: 'Skill 名称(如 my-auth-guide)',
705
+ },
706
+ },
707
+ required: ['name'],
708
+ },
709
+ },
710
+ // 38. 更新项目级 Skill
711
+ {
712
+ name: 'autosnippet_update_skill',
713
+ description:
714
+ '更新已存在的项目级 Skill 的描述或内容。\n' +
715
+ '⚠️ 内置 Skill 不可更新。更新后自动刷新编辑器索引。\n' +
716
+ '\n' +
717
+ '使用场景:\n' +
718
+ ' • 迭代改进已有 Skill 的操作指南\n' +
719
+ ' • 更新过时的最佳实践内容\n' +
720
+ ' • 修正 Skill 描述或补充新章节',
721
+ inputSchema: {
722
+ type: 'object',
723
+ properties: {
724
+ name: {
725
+ type: 'string',
726
+ description: 'Skill 名称(必须已存在于项目级 Skills 中)',
727
+ },
728
+ description: {
729
+ type: 'string',
730
+ description: '新的一句话描述(可选,不传则保持原值)',
731
+ },
732
+ content: {
733
+ type: 'string',
734
+ description: '新的正文内容(Markdown 格式,不含 frontmatter)。不传则保持原值',
735
+ },
736
+ },
737
+ required: ['name'],
738
+ },
739
+ },
687
740
  // 36. ② 内容润色:Bootstrap 候选 AI 精炼(Phase 6)
688
741
  {
689
742
  name: 'autosnippet_bootstrap_refine',
@@ -22,7 +22,7 @@ const MAX_BATCH_SIZE = 100;
22
22
  router.get('/', asyncHandler(async (req, res) => {
23
23
  const { status, language, category, keyword } = req.query;
24
24
  const page = safeInt(req.query.page, 1);
25
- const pageSize = safeInt(req.query.limit, 20, 1, 100);
25
+ const pageSize = safeInt(req.query.limit, 20, 1, 1000);
26
26
 
27
27
  const container = getServiceContainer();
28
28
  const candidateService = container.get('candidateService');
@@ -9,6 +9,9 @@ const __dirname = path.dirname(__filename);
9
9
 
10
10
  /**
11
11
  * DatabaseConnection - 数据库连接管理器
12
+ *
13
+ * 重要:相对 DB 路径通过 projectRoot 解析,而非 process.cwd()。
14
+ * 这样即使 MCP 服务器的 cwd 不是项目目录,DB 也不会创建到项目外。
12
15
  */
13
16
  export class DatabaseConnection {
14
17
  constructor(config) {
@@ -22,8 +25,14 @@ export class DatabaseConnection {
22
25
  async connect() {
23
26
  const dbPath = this.config.path;
24
27
 
28
+ // 使用 projectRoot(PathGuard 已配置)优先解析相对路径,
29
+ // 而非 path.resolve()(依赖 cwd,MCP 场景下 cwd 可能是用户主目录)
30
+ const projectRoot = pathGuard.projectRoot;
31
+ const resolvedDbPath = (projectRoot && !path.isAbsolute(dbPath))
32
+ ? path.resolve(projectRoot, dbPath)
33
+ : path.resolve(dbPath);
34
+
25
35
  // 路径安全检查 — 防止 DB 文件创建到项目允许范围外
26
- const resolvedDbPath = path.resolve(dbPath);
27
36
  pathGuard.assertProjectWriteSafe(resolvedDbPath);
28
37
 
29
38
  // 确保数据目录存在
@@ -32,7 +41,7 @@ export class DatabaseConnection {
32
41
  fs.mkdirSync(dbDir, { recursive: true });
33
42
  }
34
43
 
35
- this.db = new Database(dbPath, {
44
+ this.db = new Database(resolvedDbPath, {
36
45
  verbose: this.config.verbose ? (msg) => process.stderr.write(`[SQL] ${msg}\n`) : null,
37
46
  });
38
47
 
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * AlinkHandler — 处理 alink 指令
3
+ *
4
+ * 解析编辑器中的 alink 触发行,提取 completionKey,
5
+ * 通过数据库查找匹配的 Recipe,打开 Dashboard 详情页。
3
6
  */
4
7
 
8
+ import { getServiceContainer } from '../../../injection/ServiceContainer.js';
9
+
5
10
  /**
6
11
  * @param {string} alinkLine
7
12
  */
@@ -19,11 +24,45 @@ export async function handleAlink(alinkLine) {
19
24
 
20
25
  if (completionKey != null) {
21
26
  try {
27
+ // 从 DI 容器获取数据库实例,查找匹配 trigger 的 Recipe
28
+ const container = getServiceContainer();
29
+ const db = container.get('database');
30
+
31
+ let recipeId = null;
32
+ if (db) {
33
+ try {
34
+ const row = db.prepare(
35
+ 'SELECT id FROM recipes WHERE trigger = ? LIMIT 1',
36
+ ).get(completionKey);
37
+ if (row) recipeId = row.id;
38
+ } catch {
39
+ // DB 查询失败时回退到搜索
40
+ }
41
+
42
+ // 若精确匹配失败,尝试模糊搜索
43
+ if (!recipeId) {
44
+ try {
45
+ const escaped = completionKey.replace(/[%_\\]/g, ch => `\\${ch}`);
46
+ const row = db.prepare(
47
+ "SELECT id FROM recipes WHERE trigger LIKE ? ESCAPE '\\' OR title LIKE ? ESCAPE '\\' LIMIT 1",
48
+ ).get(`%${escaped}%`, `%${escaped}%`);
49
+ if (row) recipeId = row.id;
50
+ } catch { /* silent */ }
51
+ }
52
+ }
53
+
54
+ // 构建 Dashboard URL 并打开
55
+ const port = process.env.ASD_DASHBOARD_PORT || 3000;
56
+ const host = process.env.ASD_DASHBOARD_HOST || 'localhost';
57
+ const url = recipeId
58
+ ? `http://${host}:${port}/#/recipes/${recipeId}`
59
+ : `http://${host}:${port}/#/search?q=${encodeURIComponent(completionKey)}`;
60
+
22
61
  const open = (await import('open')).default;
23
- // TODO: 从 V2 缓存系统获取 link
24
- console.log(`[alink] completionKey=${completionKey} V2 链接缓存待实现`);
25
- } catch {
26
- // 静默
62
+ await open(url);
63
+ console.log(`[alink] completionKey=${completionKey} ${url}`);
64
+ } catch (err) {
65
+ console.warn(`[alink] Failed to open link: ${err.message}`);
27
66
  }
28
67
  }
29
68
  }
@@ -104,11 +104,11 @@ class PathGuard {
104
104
  this.#packageRoot = packageRoot ? path.resolve(packageRoot) : null;
105
105
  this.#knowledgeBaseDir = knowledgeBaseDir || null; // 延迟解析
106
106
 
107
- // 默认白名单:Xcode snippets 目录、全局缓存
107
+ // 默认白名单:Xcode snippets 目录、全局缓存(cache 子目录,不含整个 ~/.autosnippet)
108
108
  const HOME = process.env.HOME || process.env.USERPROFILE || '';
109
109
  if (HOME) {
110
110
  this.#allowList.add(path.join(HOME, 'Library/Developer/Xcode/UserData/CodeSnippets'));
111
- this.#allowList.add(path.join(HOME, '.autosnippet'));
111
+ this.#allowList.add(path.join(HOME, '.autosnippet', 'cache'));
112
112
  }
113
113
 
114
114
  // 用户自定义白名单
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autosnippet",
3
- "version": "2.7.1",
3
+ "version": "2.8.2",
4
4
  "description": "AutoSnippet - 连接开发者、AI 与项目知识库的工具",
5
5
  "type": "module",
6
6
  "main": "lib/bootstrap.js",
@@ -1,4 +1,3 @@
1
- ```skill
2
1
  ---
3
2
  name: autosnippet-coldstart
4
3
  description: Cold-start knowledge base initialization. Full 9-dimension analysis workflow — works with both external Agent and internal AI. Call bootstrap_knowledge, then systematically analyze code and submit candidates.
@@ -465,4 +464,3 @@ description: Cold-start knowledge base initialization. Full 9-dimension analysis
465
464
  - **autosnippet-reference-swift**: Swift 业界最佳实践参考(命名/并发/错误处理/内存/设计模式)
466
465
  - **autosnippet-reference-objc**: Objective-C 业界最佳实践参考(ARC/Block/Delegate/Prefix/GCD)
467
466
  - **autosnippet-reference-jsts**: JavaScript/TypeScript 业界最佳实践参考(async/React/Node/ESM/类型系统)
468
- ```
@@ -1,4 +1,3 @@
1
- ```skill
2
1
  ---
3
2
  name: autosnippet-reference-jsts
4
3
  description: JavaScript/TypeScript 业界最佳实践参考。涵盖类型系统、模块化、错误处理、async/await、命名、ESLint 规则,为冷启动分析提供高质量参考标准。
@@ -437,4 +436,3 @@ const url = baseUrl + '/api/v' + version + '/users/' + userId;
437
436
  - **autosnippet-coldstart**: 完整冷启动流程(本 Skill 的主 Skill)
438
437
  - **autosnippet-reference-swift**: Swift 业界最佳实践参考
439
438
  - **autosnippet-reference-objc**: Objective-C 业界最佳实践参考
440
- ```
@@ -1,4 +1,3 @@
1
- ```skill
2
1
  ---
3
2
  name: autosnippet-reference-objc
4
3
  description: Objective-C 业界最佳实践参考。涵盖命名前缀、属性声明、Delegate 模式、内存管理、nullability、错误处理,为冷启动分析提供高质量参考标准。
@@ -424,4 +423,3 @@ if ([NSThread isMainThread]) {
424
423
  - **autosnippet-coldstart**: 完整冷启动流程(本 Skill 的主 Skill)
425
424
  - **autosnippet-reference-swift**: Swift 业界最佳实践参考
426
425
  - **autosnippet-reference-jsts**: JavaScript/TypeScript 业界最佳实践参考
427
- ```
@@ -1,4 +1,3 @@
1
- ```skill
2
1
  ---
3
2
  name: autosnippet-reference-swift
4
3
  description: Swift 业界最佳实践参考。涵盖命名规范、Swift Concurrency、SwiftUI 模式、错误处理、内存管理、设计模式,为冷启动分析提供高质量参考标准。
@@ -458,4 +457,3 @@ UIView.animate(
458
457
  - **autosnippet-coldstart**: 完整冷启动流程(本 Skill 的主 Skill)
459
458
  - **autosnippet-reference-objc**: Objective-C 业界最佳实践参考
460
459
  - **autosnippet-reference-jsts**: JavaScript/TypeScript 业界最佳实践参考
461
- ```