autosnippet 2.7.1 → 2.8.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.
@@ -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">
@@ -148,10 +148,12 @@ export class McpServer {
148
148
  // Bootstrap 冷启动
149
149
  case 'autosnippet_bootstrap_knowledge': return bootstrapHandlers.bootstrapKnowledge(ctx, args);
150
150
  case 'autosnippet_bootstrap_refine': return bootstrapHandlers.bootstrapRefine(ctx, args);
151
- // Skills 加载 & 创建 & 推荐
151
+ // Skills 加载 & 创建 & 管理 & 推荐
152
152
  case 'autosnippet_list_skills': return skillHandlers.listSkills();
153
153
  case 'autosnippet_load_skill': return skillHandlers.loadSkill(ctx, args);
154
154
  case 'autosnippet_create_skill': return skillHandlers.createSkill(ctx, args);
155
+ case 'autosnippet_delete_skill': return skillHandlers.deleteSkill(ctx, args);
156
+ case 'autosnippet_update_skill': return skillHandlers.updateSkill(ctx, args);
155
157
  case 'autosnippet_suggest_skills': return skillHandlers.suggestSkills(ctx);
156
158
  default: throw new Error(`Unknown tool: ${name}`);
157
159
  }
@@ -200,9 +202,9 @@ export class McpServer {
200
202
  await this.initialize();
201
203
  const transport = new StdioServerTransport();
202
204
  await this.server.connect(transport);
203
- this.logger.info('MCP Server started (stdio) — 31 tools');
205
+ this.logger.info('MCP Server started (stdio) — 38 tools');
204
206
  // 在 stderr 写一行简洁的就绪通知(不使用 winston,仅用于 Cursor 日志面板 & 调试)
205
- process.stderr.write('AutoSnippet MCP ready — 31 tools\n');
207
+ process.stderr.write('AutoSnippet MCP ready — 38 tools\n');
206
208
  }
207
209
 
208
210
  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',
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autosnippet",
3
- "version": "2.7.1",
3
+ "version": "2.8.0",
4
4
  "description": "AutoSnippet - 连接开发者、AI 与项目知识库的工具",
5
5
  "type": "module",
6
6
  "main": "lib/bootstrap.js",