local-skills-mcp 0.1.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/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +383 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.js +263 -0
- package/dist/skill-loader.d.ts +126 -0
- package/dist/skill-loader.js +231 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.js +1 -0
- package/package.json +114 -0
- package/skills/local-skills-mcp-guide/SKILL.md +219 -0
- package/skills/local-skills-mcp-usage/SKILL.md +533 -0
- package/skills/skill-creator/SKILL.md +240 -0
- package/skills/skill-refresh-helper/SKILL.md +192 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import os from "os";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { SkillLoader } from "./skill-loader.js";
|
|
10
|
+
// Get version from package.json (single source of truth)
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
14
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"), "utf-8"));
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
|
16
|
+
const VERSION = packageJson.version;
|
|
17
|
+
/**
|
|
18
|
+
* Get all skills directories to aggregate from.
|
|
19
|
+
*
|
|
20
|
+
* Scans for skill directories in the following order (lowest to highest priority):
|
|
21
|
+
* 1. `<package-root>/skills` - Package built-in skills (self-documenting, always available)
|
|
22
|
+
* 2. `~/.claude/skills` - Global Claude skills
|
|
23
|
+
* 3. `{cwd}/.claude/skills` - Project-level Claude skills
|
|
24
|
+
* 4. `{cwd}/skills` - Default project skills directory
|
|
25
|
+
* 5. `$SKILLS_DIR` - Custom directory from environment variable
|
|
26
|
+
*
|
|
27
|
+
* For duplicate skill names, later directories take precedence. This allows
|
|
28
|
+
* users to override built-in skills with their own versions.
|
|
29
|
+
*
|
|
30
|
+
* @returns Array of directory paths that exist on the filesystem
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const dirs = getAllSkillsDirectories();
|
|
35
|
+
* console.log(dirs);
|
|
36
|
+
* // ['/usr/lib/node_modules/local-skills-mcp/skills', '/home/user/.claude/skills', '/home/user/project/skills']
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* // With SKILLS_DIR environment variable
|
|
42
|
+
* process.env.SKILLS_DIR = '/custom/skills';
|
|
43
|
+
* const dirs = getAllSkillsDirectories();
|
|
44
|
+
* // Returns: [...standard paths..., '/custom/skills']
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function getAllSkillsDirectories() {
|
|
48
|
+
const directories = [];
|
|
49
|
+
// Always include package built-in skills first (lowest priority, always available)
|
|
50
|
+
// This provides self-documenting capabilities out-of-the-box
|
|
51
|
+
const packageRoot = path.join(__dirname, "..");
|
|
52
|
+
const packageSkills = path.join(packageRoot, "skills");
|
|
53
|
+
if (fs.existsSync(packageSkills)) {
|
|
54
|
+
directories.push(packageSkills);
|
|
55
|
+
}
|
|
56
|
+
// Include standard Claude locations if they exist
|
|
57
|
+
const homeClaudeSkills = path.join(os.homedir(), ".claude", "skills");
|
|
58
|
+
if (fs.existsSync(homeClaudeSkills)) {
|
|
59
|
+
directories.push(homeClaudeSkills);
|
|
60
|
+
}
|
|
61
|
+
const projectClaudeSkills = path.join(process.cwd(), ".claude", "skills");
|
|
62
|
+
if (fs.existsSync(projectClaudeSkills)) {
|
|
63
|
+
directories.push(projectClaudeSkills);
|
|
64
|
+
}
|
|
65
|
+
const defaultSkills = path.join(process.cwd(), "skills");
|
|
66
|
+
if (fs.existsSync(defaultSkills)) {
|
|
67
|
+
directories.push(defaultSkills);
|
|
68
|
+
}
|
|
69
|
+
// If SKILLS_DIR is set, add it (it takes precedence for duplicates)
|
|
70
|
+
if (process.env.SKILLS_DIR) {
|
|
71
|
+
directories.push(process.env.SKILLS_DIR);
|
|
72
|
+
}
|
|
73
|
+
return directories;
|
|
74
|
+
}
|
|
75
|
+
const SKILLS_DIRS = getAllSkillsDirectories();
|
|
76
|
+
/**
|
|
77
|
+
* Main MCP server class for serving skills to AI clients.
|
|
78
|
+
*
|
|
79
|
+
* LocalSkillsServer handles MCP protocol communication, manages skill discovery
|
|
80
|
+
* and loading through SkillLoader, and formats responses for clients.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const server = new LocalSkillsServer();
|
|
85
|
+
* await server.run();
|
|
86
|
+
* // Server is now running and listening on stdio
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export class LocalSkillsServer {
|
|
90
|
+
server;
|
|
91
|
+
skillLoader;
|
|
92
|
+
/**
|
|
93
|
+
* Creates a new LocalSkillsServer instance.
|
|
94
|
+
*
|
|
95
|
+
* Initializes the MCP server with capabilities, creates a SkillLoader
|
|
96
|
+
* for all configured directories, and sets up request handlers.
|
|
97
|
+
*/
|
|
98
|
+
constructor() {
|
|
99
|
+
this.server = new Server({
|
|
100
|
+
name: "local-skills-mcp",
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
102
|
+
version: VERSION,
|
|
103
|
+
}, {
|
|
104
|
+
capabilities: {
|
|
105
|
+
tools: {},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
this.skillLoader = new SkillLoader(SKILLS_DIRS);
|
|
109
|
+
this.setupHandlers();
|
|
110
|
+
this.setupErrorHandling();
|
|
111
|
+
}
|
|
112
|
+
setupErrorHandling() {
|
|
113
|
+
this.server.onerror = (error) => {
|
|
114
|
+
console.error("[MCP Error]", error);
|
|
115
|
+
};
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
117
|
+
process.on("SIGINT", async () => {
|
|
118
|
+
await this.server.close();
|
|
119
|
+
process.exit(0);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
setupHandlers() {
|
|
123
|
+
// List available tools (dynamically generated to include current skill list)
|
|
124
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
125
|
+
// Discover skills to get current list
|
|
126
|
+
const skillNames = await this.skillLoader.discoverSkills();
|
|
127
|
+
// Build enhanced description for get_skill following Claude Skills best practices
|
|
128
|
+
// Pattern: [What it does]. [Value proposition]. Use when [trigger conditions].
|
|
129
|
+
let getSkillDescription = "Loads specialized expert prompt instructions that transform your capabilities for specific tasks. " +
|
|
130
|
+
"Each skill provides comprehensive guidance, proven methodologies, and domain-specific best practices. " +
|
|
131
|
+
"Use when you need focused expertise, systematic approaches, or professional standards for any task that would benefit from specialized knowledge. " +
|
|
132
|
+
"Invoke with the skill name to receive detailed instructions that enhance your problem-solving approach with structured, expert-level guidance.";
|
|
133
|
+
// Package built-in skills ensure there are always skills available
|
|
134
|
+
if (skillNames.length > 0) {
|
|
135
|
+
getSkillDescription += `\n\nAvailable skills: ${skillNames.join(", ")}`;
|
|
136
|
+
}
|
|
137
|
+
const tools = [
|
|
138
|
+
{
|
|
139
|
+
name: "get_skill",
|
|
140
|
+
description: getSkillDescription,
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {
|
|
144
|
+
skill_name: {
|
|
145
|
+
type: "string",
|
|
146
|
+
description: 'The name of the skill to retrieve (e.g., "code-reviewer", "test-generator")',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
required: ["skill_name"],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
return { tools };
|
|
154
|
+
});
|
|
155
|
+
// Handle tool calls
|
|
156
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
157
|
+
try {
|
|
158
|
+
switch (request.params.name) {
|
|
159
|
+
case "get_skill":
|
|
160
|
+
return await this.handleGetSkill(request.params.arguments);
|
|
161
|
+
default:
|
|
162
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
167
|
+
return {
|
|
168
|
+
content: [
|
|
169
|
+
{
|
|
170
|
+
type: "text",
|
|
171
|
+
text: `Error: ${errorMessage}`,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
179
|
+
async handleGetSkill(args) {
|
|
180
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
|
181
|
+
const skillName = args?.skill_name;
|
|
182
|
+
if (!skillName) {
|
|
183
|
+
throw new Error("skill_name is required");
|
|
184
|
+
}
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
186
|
+
const skill = await this.skillLoader.loadSkill(skillName);
|
|
187
|
+
const output = [
|
|
188
|
+
`# Skill: ${skill.name}`,
|
|
189
|
+
``,
|
|
190
|
+
`**Description:** ${skill.description}`,
|
|
191
|
+
`**Source:** ${skill.source}`,
|
|
192
|
+
``,
|
|
193
|
+
`---`,
|
|
194
|
+
``,
|
|
195
|
+
skill.content,
|
|
196
|
+
];
|
|
197
|
+
return {
|
|
198
|
+
content: [
|
|
199
|
+
{
|
|
200
|
+
type: "text",
|
|
201
|
+
text: output.join("\n"),
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Starts the MCP server and connects it to stdio transport.
|
|
208
|
+
*
|
|
209
|
+
* This method initializes the stdio transport for MCP communication,
|
|
210
|
+
* connects the server, and logs startup information including the
|
|
211
|
+
* list of skill directories being monitored.
|
|
212
|
+
*
|
|
213
|
+
* @throws {Error} If the server fails to connect or start
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```typescript
|
|
217
|
+
* const server = new LocalSkillsServer();
|
|
218
|
+
* await server.run();
|
|
219
|
+
* // Output to stderr:
|
|
220
|
+
* // Local Skills MCP Server v0.1.0 running on stdio
|
|
221
|
+
* // Aggregating skills from 3 directories:
|
|
222
|
+
* // - /home/user/.claude/skills
|
|
223
|
+
* // - /home/user/project/.claude/skills
|
|
224
|
+
* // - /home/user/project/skills
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
async run() {
|
|
228
|
+
const transport = new StdioServerTransport();
|
|
229
|
+
await this.server.connect(transport);
|
|
230
|
+
console.error(`Local Skills MCP Server v${VERSION} running on stdio`);
|
|
231
|
+
console.error(`Aggregating skills from ${SKILLS_DIRS.length} director${SKILLS_DIRS.length === 1 ? "y" : "ies"}:`);
|
|
232
|
+
SKILLS_DIRS.forEach((dir) => console.error(` - ${dir}`));
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Closes the server and releases all resources.
|
|
236
|
+
*
|
|
237
|
+
* This method properly shuts down the MCP server and waits for all
|
|
238
|
+
* file handles and resources to be released. This is particularly
|
|
239
|
+
* important on Windows where file handles may take longer to release.
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* const server = new LocalSkillsServer();
|
|
244
|
+
* await server.run();
|
|
245
|
+
* // ... do work ...
|
|
246
|
+
* await server.close();
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
async close() {
|
|
250
|
+
await this.server.close();
|
|
251
|
+
// Allow time for all handles to be released (important on Windows)
|
|
252
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Start the server only if this module is being run directly
|
|
256
|
+
/* istanbul ignore if */
|
|
257
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
258
|
+
const server = new LocalSkillsServer();
|
|
259
|
+
server.run().catch((error) => {
|
|
260
|
+
console.error("Fatal error running server:", error);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Skill, SkillMetadata } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Manages skill discovery and loading across multiple directories.
|
|
4
|
+
*
|
|
5
|
+
* SkillLoader is responsible for:
|
|
6
|
+
* - Discovering skills in configured directories
|
|
7
|
+
* - Loading and parsing SKILL.md files with YAML frontmatter
|
|
8
|
+
* - Managing the skill registry for fast lookups
|
|
9
|
+
*
|
|
10
|
+
* Skills are loaded fresh from disk on each request to support hot reload.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const loader = new SkillLoader([
|
|
15
|
+
* '/home/user/.claude/skills',
|
|
16
|
+
* '/home/user/project/skills'
|
|
17
|
+
* ]);
|
|
18
|
+
* const skills = await loader.discoverSkills();
|
|
19
|
+
* const skill = await loader.loadSkill('code-reviewer');
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare class SkillLoader {
|
|
23
|
+
private skillsPaths;
|
|
24
|
+
private skillRegistry;
|
|
25
|
+
/**
|
|
26
|
+
* Creates a new SkillLoader instance.
|
|
27
|
+
*
|
|
28
|
+
* @param skillsPaths - Array of directory paths to search for skills
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const loader = new SkillLoader([
|
|
33
|
+
* '/home/user/.claude/skills',
|
|
34
|
+
* '/home/user/project/skills'
|
|
35
|
+
* ]);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
constructor(skillsPaths: string[]);
|
|
39
|
+
/**
|
|
40
|
+
* Parse SKILL.md file with YAML frontmatter
|
|
41
|
+
*/
|
|
42
|
+
private parseSkillFile;
|
|
43
|
+
/**
|
|
44
|
+
* Discover all available skills aggregated from all directories.
|
|
45
|
+
*
|
|
46
|
+
* Scans all configured directories for subdirectories containing SKILL.md files.
|
|
47
|
+
* Later directories override earlier ones for duplicate skill names.
|
|
48
|
+
* The internal skill registry is rebuilt on each call.
|
|
49
|
+
*
|
|
50
|
+
* @returns Promise resolving to a sorted array of skill names
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const skillNames = await loader.discoverSkills();
|
|
55
|
+
* console.log(skillNames);
|
|
56
|
+
* // Output: ['code-reviewer', 'test-generator', 'refactoring-expert']
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
discoverSkills(): Promise<string[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Load a specific skill by name, reading fresh from disk each time.
|
|
62
|
+
*
|
|
63
|
+
* Loads the SKILL.md file, parses YAML frontmatter, and validates metadata.
|
|
64
|
+
* Skills are always read fresh to support hot reload of content changes.
|
|
65
|
+
*
|
|
66
|
+
* @param skillName - The name of the skill to load
|
|
67
|
+
* @returns Promise resolving to the complete Skill object
|
|
68
|
+
* @throws {Error} If skill is not found in registry or file cannot be loaded/parsed
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const skill = await loader.loadSkill('code-reviewer');
|
|
73
|
+
* console.log(skill.name); // 'Code Reviewer'
|
|
74
|
+
* console.log(skill.description); // 'Expert code review assistant'
|
|
75
|
+
* console.log(skill.content); // Full skill prompt content
|
|
76
|
+
* console.log(skill.source); // '/home/user/.claude/skills'
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* // Error handling
|
|
82
|
+
* try {
|
|
83
|
+
* const skill = await loader.loadSkill('non-existent');
|
|
84
|
+
* } catch (error) {
|
|
85
|
+
* console.error(error.message);
|
|
86
|
+
* // "Skill "non-existent" not found. Run list_skills to see available skills."
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
loadSkill(skillName: string): Promise<Skill>;
|
|
91
|
+
/**
|
|
92
|
+
* Get skill metadata without loading the full content.
|
|
93
|
+
*
|
|
94
|
+
* Lightweight operation that loads only the YAML frontmatter without
|
|
95
|
+
* the full skill content. Useful for listing skills with descriptions.
|
|
96
|
+
*
|
|
97
|
+
* @param skillName - The name of the skill
|
|
98
|
+
* @returns Promise resolving to metadata with source directory
|
|
99
|
+
* @throws {Error} If skill is not found or metadata cannot be loaded
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* const metadata = await loader.getSkillMetadata('code-reviewer');
|
|
104
|
+
* console.log(metadata.name); // 'Code Reviewer'
|
|
105
|
+
* console.log(metadata.description); // 'Expert code review assistant'
|
|
106
|
+
* console.log(metadata.source); // '/home/user/.claude/skills'
|
|
107
|
+
* // Note: metadata does not include 'content' field
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
getSkillMetadata(skillName: string): Promise<SkillMetadata & {
|
|
111
|
+
source: string;
|
|
112
|
+
}>;
|
|
113
|
+
/**
|
|
114
|
+
* Get all skills directories being monitored.
|
|
115
|
+
*
|
|
116
|
+
* @returns Array of directory paths configured for this loader
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* const paths = loader.getSkillsPaths();
|
|
121
|
+
* console.log(paths);
|
|
122
|
+
* // Output: ['/home/user/.claude/skills', '/home/user/project/skills']
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
getSkillsPaths(): string[];
|
|
126
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
/**
|
|
5
|
+
* Manages skill discovery and loading across multiple directories.
|
|
6
|
+
*
|
|
7
|
+
* SkillLoader is responsible for:
|
|
8
|
+
* - Discovering skills in configured directories
|
|
9
|
+
* - Loading and parsing SKILL.md files with YAML frontmatter
|
|
10
|
+
* - Managing the skill registry for fast lookups
|
|
11
|
+
*
|
|
12
|
+
* Skills are loaded fresh from disk on each request to support hot reload.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const loader = new SkillLoader([
|
|
17
|
+
* '/home/user/.claude/skills',
|
|
18
|
+
* '/home/user/project/skills'
|
|
19
|
+
* ]);
|
|
20
|
+
* const skills = await loader.discoverSkills();
|
|
21
|
+
* const skill = await loader.loadSkill('code-reviewer');
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class SkillLoader {
|
|
25
|
+
skillsPaths;
|
|
26
|
+
skillRegistry = new Map();
|
|
27
|
+
/**
|
|
28
|
+
* Creates a new SkillLoader instance.
|
|
29
|
+
*
|
|
30
|
+
* @param skillsPaths - Array of directory paths to search for skills
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const loader = new SkillLoader([
|
|
35
|
+
* '/home/user/.claude/skills',
|
|
36
|
+
* '/home/user/project/skills'
|
|
37
|
+
* ]);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
constructor(skillsPaths) {
|
|
41
|
+
this.skillsPaths = skillsPaths;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse SKILL.md file with YAML frontmatter
|
|
45
|
+
*/
|
|
46
|
+
parseSkillFile(content) {
|
|
47
|
+
// Check if file starts with frontmatter delimiter
|
|
48
|
+
if (!content.startsWith("---\n")) {
|
|
49
|
+
throw new Error("SKILL.md must start with YAML frontmatter (---)");
|
|
50
|
+
}
|
|
51
|
+
// Find the closing delimiter
|
|
52
|
+
const endDelimiterIndex = content.indexOf("\n---\n", 4);
|
|
53
|
+
if (endDelimiterIndex === -1) {
|
|
54
|
+
throw new Error("SKILL.md frontmatter must end with --- delimiter");
|
|
55
|
+
}
|
|
56
|
+
// Extract and parse YAML frontmatter
|
|
57
|
+
const frontmatterText = content.substring(4, endDelimiterIndex);
|
|
58
|
+
const metadata = YAML.parse(frontmatterText);
|
|
59
|
+
// Validate required fields
|
|
60
|
+
if (!metadata.name) {
|
|
61
|
+
throw new Error('SKILL.md frontmatter must include "name" field');
|
|
62
|
+
}
|
|
63
|
+
if (!metadata.description) {
|
|
64
|
+
throw new Error('SKILL.md frontmatter must include "description" field');
|
|
65
|
+
}
|
|
66
|
+
// Extract content after frontmatter
|
|
67
|
+
const skillContent = content.substring(endDelimiterIndex + 5).trim();
|
|
68
|
+
return { metadata, content: skillContent };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Discover all available skills aggregated from all directories.
|
|
72
|
+
*
|
|
73
|
+
* Scans all configured directories for subdirectories containing SKILL.md files.
|
|
74
|
+
* Later directories override earlier ones for duplicate skill names.
|
|
75
|
+
* The internal skill registry is rebuilt on each call.
|
|
76
|
+
*
|
|
77
|
+
* @returns Promise resolving to a sorted array of skill names
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const skillNames = await loader.discoverSkills();
|
|
82
|
+
* console.log(skillNames);
|
|
83
|
+
* // Output: ['code-reviewer', 'test-generator', 'refactoring-expert']
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
async discoverSkills() {
|
|
87
|
+
this.skillRegistry.clear();
|
|
88
|
+
const allSkills = new Map();
|
|
89
|
+
// Iterate through all skill directories
|
|
90
|
+
for (const skillsPath of this.skillsPaths) {
|
|
91
|
+
try {
|
|
92
|
+
const entries = await fs.readdir(skillsPath, { withFileTypes: true });
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
const skillPath = path.join(skillsPath, entry.name);
|
|
96
|
+
const skillFilePath = path.join(skillPath, "SKILL.md");
|
|
97
|
+
try {
|
|
98
|
+
await fs.access(skillFilePath);
|
|
99
|
+
// Later directories override earlier ones for duplicate names
|
|
100
|
+
allSkills.set(entry.name, {
|
|
101
|
+
path: skillPath,
|
|
102
|
+
source: skillsPath,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Skip directories without SKILL.md
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error.code !== "ENOENT") {
|
|
114
|
+
console.error(`Error reading directory ${skillsPath}:`, error);
|
|
115
|
+
}
|
|
116
|
+
// Continue with other directories even if one fails
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Update registry
|
|
121
|
+
this.skillRegistry = allSkills;
|
|
122
|
+
// Return sorted skill names
|
|
123
|
+
return Array.from(allSkills.keys()).sort();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Load a specific skill by name, reading fresh from disk each time.
|
|
127
|
+
*
|
|
128
|
+
* Loads the SKILL.md file, parses YAML frontmatter, and validates metadata.
|
|
129
|
+
* Skills are always read fresh to support hot reload of content changes.
|
|
130
|
+
*
|
|
131
|
+
* @param skillName - The name of the skill to load
|
|
132
|
+
* @returns Promise resolving to the complete Skill object
|
|
133
|
+
* @throws {Error} If skill is not found in registry or file cannot be loaded/parsed
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```typescript
|
|
137
|
+
* const skill = await loader.loadSkill('code-reviewer');
|
|
138
|
+
* console.log(skill.name); // 'Code Reviewer'
|
|
139
|
+
* console.log(skill.description); // 'Expert code review assistant'
|
|
140
|
+
* console.log(skill.content); // Full skill prompt content
|
|
141
|
+
* console.log(skill.source); // '/home/user/.claude/skills'
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* // Error handling
|
|
147
|
+
* try {
|
|
148
|
+
* const skill = await loader.loadSkill('non-existent');
|
|
149
|
+
* } catch (error) {
|
|
150
|
+
* console.error(error.message);
|
|
151
|
+
* // "Skill "non-existent" not found. Run list_skills to see available skills."
|
|
152
|
+
* }
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
async loadSkill(skillName) {
|
|
156
|
+
// Get skill location from registry
|
|
157
|
+
const skillInfo = this.skillRegistry.get(skillName);
|
|
158
|
+
if (!skillInfo) {
|
|
159
|
+
throw new Error(`Skill "${skillName}" not found. Run list_skills to see available skills.`);
|
|
160
|
+
}
|
|
161
|
+
const skillFilePath = path.join(skillInfo.path, "SKILL.md");
|
|
162
|
+
try {
|
|
163
|
+
// Load and parse SKILL.md fresh from disk
|
|
164
|
+
const fileContent = await fs.readFile(skillFilePath, "utf-8");
|
|
165
|
+
const { metadata, content } = this.parseSkillFile(fileContent);
|
|
166
|
+
const skill = {
|
|
167
|
+
name: metadata.name,
|
|
168
|
+
description: metadata.description,
|
|
169
|
+
content,
|
|
170
|
+
path: skillInfo.path,
|
|
171
|
+
source: skillInfo.source,
|
|
172
|
+
};
|
|
173
|
+
return skill;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
throw new Error(`Failed to load skill "${skillName}": ${error.message}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get skill metadata without loading the full content.
|
|
181
|
+
*
|
|
182
|
+
* Lightweight operation that loads only the YAML frontmatter without
|
|
183
|
+
* the full skill content. Useful for listing skills with descriptions.
|
|
184
|
+
*
|
|
185
|
+
* @param skillName - The name of the skill
|
|
186
|
+
* @returns Promise resolving to metadata with source directory
|
|
187
|
+
* @throws {Error} If skill is not found or metadata cannot be loaded
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* const metadata = await loader.getSkillMetadata('code-reviewer');
|
|
192
|
+
* console.log(metadata.name); // 'Code Reviewer'
|
|
193
|
+
* console.log(metadata.description); // 'Expert code review assistant'
|
|
194
|
+
* console.log(metadata.source); // '/home/user/.claude/skills'
|
|
195
|
+
* // Note: metadata does not include 'content' field
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
async getSkillMetadata(skillName) {
|
|
199
|
+
const skillInfo = this.skillRegistry.get(skillName);
|
|
200
|
+
if (!skillInfo) {
|
|
201
|
+
throw new Error(`Skill "${skillName}" not found`);
|
|
202
|
+
}
|
|
203
|
+
const skillFilePath = path.join(skillInfo.path, "SKILL.md");
|
|
204
|
+
try {
|
|
205
|
+
const fileContent = await fs.readFile(skillFilePath, "utf-8");
|
|
206
|
+
const { metadata } = this.parseSkillFile(fileContent);
|
|
207
|
+
return {
|
|
208
|
+
...metadata,
|
|
209
|
+
source: skillInfo.source,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
throw new Error(`Failed to load metadata for skill "${skillName}": ${error.message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get all skills directories being monitored.
|
|
218
|
+
*
|
|
219
|
+
* @returns Array of directory paths configured for this loader
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```typescript
|
|
223
|
+
* const paths = loader.getSkillsPaths();
|
|
224
|
+
* console.log(paths);
|
|
225
|
+
* // Output: ['/home/user/.claude/skills', '/home/user/project/skills']
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
getSkillsPaths() {
|
|
229
|
+
return this.skillsPaths;
|
|
230
|
+
}
|
|
231
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill metadata from YAML frontmatter in SKILL.md
|
|
3
|
+
*/
|
|
4
|
+
export interface SkillMetadata {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Full skill definition including prompt content
|
|
10
|
+
*/
|
|
11
|
+
export interface Skill {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
content: string;
|
|
15
|
+
path: string;
|
|
16
|
+
source: string;
|
|
17
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|