@templmf/temp-solf-lmf 0.0.42 → 0.0.43

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@templmf/temp-solf-lmf",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,13 +1,10 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import {
4
- CallToolRequestSchema,
5
- ListToolsRequestSchema,
6
- } from "@modelcontextprotocol/sdk/types.js";
7
3
  import * as fs from "fs";
8
4
  import * as path from "path";
9
5
  import * as os from "os";
10
6
  import matter from "gray-matter";
7
+ import { z } from "zod";
11
8
 
12
9
  // ──────────────────────────────────────────────
13
10
  // 路径解析:支持项目级 + 全局两个 Skills 目录
@@ -16,18 +13,17 @@ import matter from "gray-matter";
16
13
  function getSkillDirs(): string[] {
17
14
  const dirs: string[] = [];
18
15
 
19
- // 全局路径:%USERPROFILE%\.roo\skills Windows)或 ~/.roo/skills(Linux/macOS
16
+ // 全局路径:~/.roo/skills(Linux/macOS)或 %USERPROFILE%\.roo\skills(Windows
20
17
  const globalDir = path.join(os.homedir(), ".roo", "skills");
21
18
  if (fs.existsSync(globalDir)) dirs.push(globalDir);
22
19
 
23
- // 项目路径:cwd()\.roo\skills
20
+ // 项目路径:cwd()/.roo/skills
24
21
  const projectDir = path.join(process.cwd(), ".roo", "skills");
25
22
  if (fs.existsSync(projectDir) && projectDir !== globalDir) {
26
23
  dirs.push(projectDir);
27
24
  }
28
25
 
29
26
  // 支持通过环境变量追加自定义路径,多个路径用分号分隔
30
- // 例:SKILLS_EXTRA_DIRS=C:\my-skills;D:\shared-skills
31
27
  if (process.env.SKILLS_EXTRA_DIRS) {
32
28
  for (const d of process.env.SKILLS_EXTRA_DIRS.split(";")) {
33
29
  const trimmed = d.trim();
@@ -53,7 +49,7 @@ interface SkillMeta {
53
49
  }
54
50
 
55
51
  // ──────────────────────────────────────────────
56
- // 核心:扫描所有 Skills
52
+ // 核心:扫描所有 Skills(用于 list / search)
57
53
  // ──────────────────────────────────────────────
58
54
 
59
55
  function scanSkills(): SkillMeta[] {
@@ -61,17 +57,10 @@ function scanSkills(): SkillMeta[] {
61
57
  const skillMap = new Map<string, SkillMeta>(); // name → meta,项目级覆盖全局
62
58
 
63
59
  for (const baseDir of skillDirs) {
64
- // 判断来源标签
65
- const globalDir = path.join(os.homedir(), ".roo", "skills");
66
- const projectDir = path.join(process.cwd(), ".roo", "skills");
67
- let source = "custom";
68
- if (baseDir === globalDir) source = "global";
69
- else if (baseDir === projectDir) source = "project";
70
-
60
+ const source = resolveSource(baseDir);
71
61
  if (!fs.existsSync(baseDir)) continue;
72
62
 
73
- const entries = fs.readdirSync(baseDir, { withFileTypes: true });
74
- for (const entry of entries) {
63
+ for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
75
64
  if (!entry.isDirectory()) continue;
76
65
 
77
66
  const skillDir = path.join(baseDir, entry.name);
@@ -79,13 +68,10 @@ function scanSkills(): SkillMeta[] {
79
68
  if (!fs.existsSync(skillFile)) continue;
80
69
 
81
70
  try {
82
- const raw = fs.readFileSync(skillFile, "utf-8");
83
- const { data } = matter(raw);
84
-
71
+ const { data } = matter(fs.readFileSync(skillFile, "utf-8"));
85
72
  const name = (data.name as string | undefined)?.trim() ?? entry.name;
86
73
  const description = (data.description as string | undefined)?.trim() ?? "";
87
-
88
- // 项目级 > 全局(后扫描的覆盖先扫描的,skillDirs 里项目在后)
74
+ // 项目级 > 全局(后扫描的覆盖先扫描的)
89
75
  skillMap.set(name, { name, description, skillDir, skillFile, source });
90
76
  } catch {
91
77
  // 解析失败跳过
@@ -93,15 +79,68 @@ function scanSkills(): SkillMeta[] {
93
79
  }
94
80
  }
95
81
 
96
- return Array.from(skillMap.values()).sort((a, b) =>
97
- a.name.localeCompare(b.name)
98
- );
82
+ return Array.from(skillMap.values()).sort((a, b) => a.name.localeCompare(b.name));
83
+ }
84
+
85
+ // ──────────────────────────────────────────────
86
+ // 直接按名称查找 Skill(无需全量扫描)
87
+ // 优先级:project > global > custom
88
+ // ──────────────────────────────────────────────
89
+
90
+ function findSkillByName(skillName: string): SkillMeta | null {
91
+ const skillDirs = getSkillDirs();
92
+
93
+ // 倒序遍历使项目级优先(skillDirs 里项目在后)
94
+ for (const baseDir of [...skillDirs].reverse()) {
95
+ if (!fs.existsSync(baseDir)) continue;
96
+
97
+ // 先尝试以 skillName 作为目录名直接定位
98
+ const directDir = path.join(baseDir, skillName);
99
+ const directFile = path.join(directDir, "SKILL.md");
100
+ if (fs.existsSync(directFile)) {
101
+ try {
102
+ const { data } = matter(fs.readFileSync(directFile, "utf-8"));
103
+ const name = (data.name as string | undefined)?.trim() ?? skillName;
104
+ const description = (data.description as string | undefined)?.trim() ?? "";
105
+ return { name, description, skillDir: directDir, skillFile: directFile, source: resolveSource(baseDir) };
106
+ } catch {
107
+ // 继续尝试其他路径
108
+ }
109
+ }
110
+
111
+ // 若目录名不匹配,遍历子目录对比 frontmatter name
112
+ for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
113
+ if (!entry.isDirectory()) continue;
114
+ const skillDir = path.join(baseDir, entry.name);
115
+ const skillFile = path.join(skillDir, "SKILL.md");
116
+ if (!fs.existsSync(skillFile)) continue;
117
+
118
+ try {
119
+ const { data } = matter(fs.readFileSync(skillFile, "utf-8"));
120
+ const name = (data.name as string | undefined)?.trim() ?? entry.name;
121
+ if (name === skillName) {
122
+ const description = (data.description as string | undefined)?.trim() ?? "";
123
+ return { name, description, skillDir, skillFile, source: resolveSource(baseDir) };
124
+ }
125
+ } catch {
126
+ // 跳过
127
+ }
128
+ }
129
+ }
130
+
131
+ return null;
99
132
  }
100
133
 
101
134
  // ──────────────────────────────────────────────
102
135
  // 工具函数
103
136
  // ──────────────────────────────────────────────
104
137
 
138
+ function resolveSource(baseDir: string): string {
139
+ if (baseDir === path.join(os.homedir(), ".roo", "skills")) return "global";
140
+ if (baseDir === path.join(process.cwd(), ".roo", "skills")) return "project";
141
+ return "custom";
142
+ }
143
+
105
144
  /** 列出所有文件(含子目录),返回相对路径列表 */
106
145
  function listFilesRecursive(dir: string, base: string = dir): string[] {
107
146
  const result: string[] = [];
@@ -109,11 +148,10 @@ function listFilesRecursive(dir: string, base: string = dir): string[] {
109
148
 
110
149
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
111
150
  const full = path.join(dir, entry.name);
112
- const rel = path.relative(base, full);
113
151
  if (entry.isDirectory()) {
114
152
  result.push(...listFilesRecursive(full, base));
115
153
  } else {
116
- result.push(rel);
154
+ result.push(path.relative(base, full));
117
155
  }
118
156
  }
119
157
  return result;
@@ -135,211 +173,157 @@ function fuzzyMatch(needle: string, haystack: string): boolean {
135
173
  // MCP Server
136
174
  // ──────────────────────────────────────────────
137
175
 
138
- const server = new Server(
139
- { name: "skills-mcp", version: "1.0.0" },
140
- { capabilities: { tools: {} } }
141
- );
142
-
143
- // 注册工具列表
144
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
145
- tools: [
146
- {
147
- name: "list_skills",
148
- description:
149
- "列出所有可用的 Skills(来自全局 ~/.roo/skills 和项目 .roo/skills)",
150
- inputSchema: {
151
- type: "object",
152
- properties: {},
153
- required: [],
154
- },
155
- },
156
- {
157
- name: "read_skill",
158
- description: "读取指定 Skill 的完整 SKILL.md 内容",
159
- inputSchema: {
160
- type: "object",
161
- properties: {
162
- name: {
163
- type: "string",
164
- description: "Skill 名称(目录名或 frontmatter 中的 name)",
165
- },
166
- },
167
- required: ["name"],
168
- },
169
- },
170
- {
171
- name: "search_skills",
172
- description: "模糊搜索 Skills,匹配名称或描述",
173
- inputSchema: {
174
- type: "object",
175
- properties: {
176
- query: {
177
- type: "string",
178
- description: "搜索关键词",
179
- },
180
- },
181
- required: ["query"],
182
- },
183
- },
184
- {
185
- name: "list_skill_files",
186
- description: "列出指定 Skill 目录下的所有附属文件(脚本、模板等)",
187
- inputSchema: {
188
- type: "object",
189
- properties: {
190
- name: {
191
- type: "string",
192
- description: "Skill 名称",
193
- },
194
- },
195
- required: ["name"],
196
- },
197
- },
198
- ],
199
- }));
200
-
201
- // 处理工具调用
202
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
203
- const { name, arguments: args } = request.params;
176
+ const server = new McpServer({
177
+ name: "skills-mcp",
178
+ version: "1.0.0",
179
+ });
204
180
 
205
- // ── list_skills ──
206
- if (name === "list_skills") {
181
+ // ── list_skills ──
182
+ server.registerTool(
183
+ "list_skills",
184
+ {
185
+ description: "列出所有可用的 Skills(来自全局 ~/.roo/skills 和项目 .roo/skills)",
186
+ inputSchema: {},
187
+ },
188
+ async () => {
207
189
  const skills = scanSkills();
190
+
208
191
  if (skills.length === 0) {
192
+ const dirList = getSkillDirs().map((d) => ` - ${d}`).join("\n");
209
193
  return {
210
- content: [
211
- {
212
- type: "text",
213
- text: "未找到任何 Skill。\n\n请检查以下目录是否存在 SKILL.md 文件:\n" +
214
- getSkillDirs().map((d) => ` - ${d}`).join("\n"),
215
- },
216
- ],
194
+ content: [{
195
+ type: "text",
196
+ text: `未找到任何 Skill。\n\n请检查以下目录是否存在 SKILL.md 文件:\n${dirList}`,
197
+ }],
217
198
  };
218
199
  }
219
200
 
220
201
  const rows = skills.map((s) => {
221
- const desc = s.description.length > 80
222
- ? s.description.slice(0, 77) + "..."
223
- : s.description;
202
+ const desc = s.description.length > 80 ? s.description.slice(0, 77) + "..." : s.description;
224
203
  return `| \`${s.name}\` | ${desc} | ${s.source} |`;
225
204
  });
226
205
 
227
- const text =
228
- `## 可用 Skills(共 ${skills.length} 个)\n\n` +
229
- `| 名称 | 描述 | 来源 |\n` +
230
- `|------|------|------|\n` +
231
- rows.join("\n");
232
-
233
- return { content: [{ type: "text", text }] };
206
+ return {
207
+ content: [{
208
+ type: "text",
209
+ text:
210
+ `## 可用 Skills(共 ${skills.length} 个)\n\n` +
211
+ `| 名称 | 描述 | 来源 |\n|------|------|------|\n` +
212
+ rows.join("\n"),
213
+ }],
214
+ };
234
215
  }
216
+ );
235
217
 
236
- // ── read_skill ──
237
- if (name === "read_skill") {
238
- const skillName = (args as { name: string }).name.trim();
239
- const skills = scanSkills();
240
- const skill = skills.find(
241
- (s) => s.name === skillName || s.skillDir.endsWith(path.sep + skillName)
242
- );
218
+ // ── read_skill ──
219
+ server.registerTool(
220
+ "read_skill",
221
+ {
222
+ description: "读取指定 Skill 的完整 SKILL.md 内容",
223
+ inputSchema: {
224
+ name: z.string().describe("Skill 名称(目录名或 frontmatter 中的 name)"),
225
+ },
226
+ },
227
+ async ({ name: skillName }) => {
228
+ const skill = findSkillByName(skillName.trim());
243
229
 
244
230
  if (!skill) {
245
- const available = skills.map((s) => `\`${s.name}\``).join(", ");
246
231
  return {
247
- content: [
248
- {
249
- type: "text",
250
- text: `未找到 Skill \`${skillName}\`。\n\n可用的 Skills:${available || "(无)"}`,
251
- },
252
- ],
232
+ content: [{
233
+ type: "text",
234
+ text: `未找到 Skill \`${skillName}\`。\n\n请确认名称是否正确,或使用 list_skills 查看所有可用 Skills。`,
235
+ }],
253
236
  };
254
237
  }
255
238
 
256
239
  const content = fs.readFileSync(skill.skillFile, "utf-8");
257
240
  return {
258
- content: [
259
- {
260
- type: "text",
261
- text: `## Skill: ${skill.name}\n**来源**: ${skill.source} (${skill.skillFile})\n\n---\n\n${content}`,
262
- },
263
- ],
241
+ content: [{
242
+ type: "text",
243
+ text: `## Skill: ${skill.name}\n**来源**: ${skill.source} (${skill.skillFile})\n\n---\n\n${content}`,
244
+ }],
264
245
  };
265
246
  }
247
+ );
266
248
 
267
- // ── search_skills ──
268
- if (name === "search_skills") {
269
- const query = (args as { query: string }).query.trim();
249
+ // ── search_skills ──
250
+ server.registerTool(
251
+ "search_skills",
252
+ {
253
+ description: "模糊搜索 Skills,匹配名称或描述",
254
+ inputSchema: {
255
+ query: z.string().describe("搜索关键词"),
256
+ },
257
+ },
258
+ async ({ query }) => {
270
259
  const skills = scanSkills();
260
+ const q = query.trim();
271
261
 
272
262
  const matched = skills.filter(
273
263
  (s) =>
274
- fuzzyMatch(query, s.name) ||
275
- s.name.toLowerCase().includes(query.toLowerCase()) ||
276
- s.description.toLowerCase().includes(query.toLowerCase())
264
+ fuzzyMatch(q, s.name) ||
265
+ s.name.toLowerCase().includes(q.toLowerCase()) ||
266
+ s.description.toLowerCase().includes(q.toLowerCase())
277
267
  );
278
268
 
279
269
  if (matched.length === 0) {
280
270
  return {
281
- content: [
282
- {
283
- type: "text",
284
- text: `未找到匹配 "${query}" 的 Skill。`,
285
- },
286
- ],
271
+ content: [{ type: "text", text: `未找到匹配 "${q}" 的 Skill。` }],
287
272
  };
288
273
  }
289
274
 
290
275
  const rows = matched.map((s) => {
291
- const desc = s.description.length > 100
292
- ? s.description.slice(0, 97) + "..."
293
- : s.description;
276
+ const desc = s.description.length > 100 ? s.description.slice(0, 97) + "..." : s.description;
294
277
  return `| \`${s.name}\` | ${desc} | ${s.source} |`;
295
278
  });
296
279
 
297
- const text =
298
- `## 搜索结果:${matched.length} 个匹配 "${query}"\n\n` +
299
- `| 名称 | 描述 | 来源 |\n` +
300
- `|------|------|------|\n` +
301
- rows.join("\n");
302
-
303
- return { content: [{ type: "text", text }] };
280
+ return {
281
+ content: [{
282
+ type: "text",
283
+ text:
284
+ `## 搜索结果:${matched.length} 个匹配 "${q}"\n\n` +
285
+ `| 名称 | 描述 | 来源 |\n|------|------|------|\n` +
286
+ rows.join("\n"),
287
+ }],
288
+ };
304
289
  }
290
+ );
305
291
 
306
- // ── list_skill_files ──
307
- if (name === "list_skill_files") {
308
- const skillName = (args as { name: string }).name.trim();
309
- const skills = scanSkills();
310
- const skill = skills.find(
311
- (s) => s.name === skillName || s.skillDir.endsWith(path.sep + skillName)
312
- );
292
+ // ── list_skill_files ──
293
+ server.registerTool(
294
+ "list_skill_files",
295
+ {
296
+ description: "列出指定 Skill 目录下的所有附属文件(脚本、模板等)",
297
+ inputSchema: {
298
+ name: z.string().describe("Skill 名称"),
299
+ },
300
+ },
301
+ async ({ name: skillName }) => {
302
+ const skill = findSkillByName(skillName.trim());
313
303
 
314
304
  if (!skill) {
315
305
  return {
316
- content: [
317
- {
318
- type: "text",
319
- text: `未找到 Skill \`${skillName}\`。`,
320
- },
321
- ],
306
+ content: [{ type: "text", text: `未找到 Skill \`${skillName}\`。` }],
322
307
  };
323
308
  }
324
309
 
325
310
  const files = listFilesRecursive(skill.skillDir);
326
- const text =
327
- `## Skill \`${skill.name}\` 的附属文件\n` +
328
- `**路径**: ${skill.skillDir}\n\n` +
329
- (files.length === 0
330
- ? "_(无附属文件)_"
331
- : files.map((f) => `- \`${f}\``).join("\n"));
332
-
333
- return { content: [{ type: "text", text }] };
311
+ return {
312
+ content: [{
313
+ type: "text",
314
+ text:
315
+ `## Skill \`${skill.name}\` 的附属文件\n` +
316
+ `**路径**: ${skill.skillDir}\n\n` +
317
+ (files.length === 0 ? "_(无附属文件)_" : files.map((f) => `- \`${f}\``).join("\n")),
318
+ }],
319
+ };
334
320
  }
321
+ );
335
322
 
336
- return {
337
- content: [{ type: "text", text: `未知工具: ${name}` }],
338
- isError: true,
339
- };
340
- });
341
-
323
+ // ──────────────────────────────────────────────
342
324
  // 启动
325
+ // ──────────────────────────────────────────────
326
+
343
327
  async function main() {
344
328
  const transport = new StdioServerTransport();
345
329
  await server.connect(transport);
@@ -349,4 +333,4 @@ async function main() {
349
333
  main().catch((err) => {
350
334
  console.error("Fatal error:", err);
351
335
  process.exit(1);
352
- });
336
+ });