@skill-tools/core 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/LICENSE +190 -0
- package/dist/index.cjs +379 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +278 -0
- package/dist/index.d.ts +278 -0
- package/dist/index.js +370 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { readFile, stat, readdir } from 'fs/promises';
|
|
3
|
+
import { isAbsolute, resolve, dirname, basename, join } from 'path';
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
import { encodingForModel } from 'js-tiktoken';
|
|
6
|
+
|
|
7
|
+
// src/parser.ts
|
|
8
|
+
var encoder = null;
|
|
9
|
+
function getEncoder() {
|
|
10
|
+
if (!encoder) {
|
|
11
|
+
encoder = encodingForModel("gpt-4o");
|
|
12
|
+
}
|
|
13
|
+
return encoder;
|
|
14
|
+
}
|
|
15
|
+
function countTokens(text) {
|
|
16
|
+
if (text.length === 0) {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
const enc = getEncoder();
|
|
20
|
+
return enc.encode(text).length;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/parser.ts
|
|
24
|
+
var FILE_REFERENCE_PATTERN = /(?:^|\s|`)((?:scripts|references|assets)\/[\w./-]+(?:\.\w+)?)/gm;
|
|
25
|
+
var MARKDOWN_LINK_PATTERN = /\[([^\]]*)\]\((?!https?:\/\/)([^)]+)\)/g;
|
|
26
|
+
var NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]?$/;
|
|
27
|
+
var CONSECUTIVE_HYPHENS = /--/;
|
|
28
|
+
async function parseSkill(filePath) {
|
|
29
|
+
const absolutePath = isAbsolute(filePath) ? filePath : resolve(filePath);
|
|
30
|
+
const dirPath = dirname(absolutePath);
|
|
31
|
+
let rawContent;
|
|
32
|
+
try {
|
|
33
|
+
rawContent = await readFile(absolutePath, "utf-8");
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
skill: null,
|
|
38
|
+
diagnostics: [
|
|
39
|
+
{
|
|
40
|
+
ruleId: "file-readable",
|
|
41
|
+
severity: "error",
|
|
42
|
+
message: `Cannot read file: ${err instanceof Error ? err.message : String(err)}`,
|
|
43
|
+
file: absolutePath
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (rawContent.trim().length === 0) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
skill: null,
|
|
52
|
+
diagnostics: [
|
|
53
|
+
{
|
|
54
|
+
ruleId: "file-not-empty",
|
|
55
|
+
severity: "error",
|
|
56
|
+
message: "SKILL.md file is empty",
|
|
57
|
+
file: absolutePath
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return parseSkillContent(rawContent, absolutePath, dirPath);
|
|
63
|
+
}
|
|
64
|
+
function parseSkillContent(rawContent, filePath, dirPath) {
|
|
65
|
+
const diagnostics = [];
|
|
66
|
+
let hasErrors = false;
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = matter(rawContent);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
skill: null,
|
|
74
|
+
diagnostics: [
|
|
75
|
+
{
|
|
76
|
+
ruleId: "frontmatter-valid-yaml",
|
|
77
|
+
severity: "error",
|
|
78
|
+
message: `Invalid YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
|
|
79
|
+
file: filePath,
|
|
80
|
+
line: 1
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const hasFrontmatter = rawContent.trimStart().startsWith("---") && Object.keys(parsed.data).length > 0;
|
|
86
|
+
if (!hasFrontmatter) {
|
|
87
|
+
diagnostics.push({
|
|
88
|
+
ruleId: "frontmatter-required",
|
|
89
|
+
severity: "error",
|
|
90
|
+
message: "SKILL.md must have YAML frontmatter between --- delimiters",
|
|
91
|
+
file: filePath,
|
|
92
|
+
line: 1,
|
|
93
|
+
fix: "Add frontmatter at the top of the file:\n---\nname: my-skill\ndescription: A short description\n---"
|
|
94
|
+
});
|
|
95
|
+
hasErrors = true;
|
|
96
|
+
}
|
|
97
|
+
const data = parsed.data;
|
|
98
|
+
if (data.name != null && typeof data.name !== "string") {
|
|
99
|
+
diagnostics.push({
|
|
100
|
+
ruleId: "name-type",
|
|
101
|
+
severity: "error",
|
|
102
|
+
message: 'Frontmatter "name" field must be a string',
|
|
103
|
+
file: filePath,
|
|
104
|
+
line: 1,
|
|
105
|
+
fix: "Ensure the name is a string: name: my-skill-name"
|
|
106
|
+
});
|
|
107
|
+
hasErrors = true;
|
|
108
|
+
} else if (data.name == null || typeof data.name === "string" && data.name.trim().length === 0) {
|
|
109
|
+
diagnostics.push({
|
|
110
|
+
ruleId: "name-required",
|
|
111
|
+
severity: "error",
|
|
112
|
+
message: 'Frontmatter must contain a "name" field (required by spec)',
|
|
113
|
+
file: filePath,
|
|
114
|
+
line: 1,
|
|
115
|
+
fix: "Add a name field: name: my-skill-name"
|
|
116
|
+
});
|
|
117
|
+
hasErrors = true;
|
|
118
|
+
} else if (typeof data.name === "string") {
|
|
119
|
+
if (!NAME_PATTERN.test(data.name)) {
|
|
120
|
+
diagnostics.push({
|
|
121
|
+
ruleId: "name-format",
|
|
122
|
+
severity: "error",
|
|
123
|
+
message: `Skill name "${data.name}" is invalid. Must be 1-64 chars, lowercase letters, numbers, and hyphens only. Must not start or end with a hyphen`,
|
|
124
|
+
file: filePath,
|
|
125
|
+
line: 1,
|
|
126
|
+
fix: `Use a name like: ${String(data.name).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}`
|
|
127
|
+
});
|
|
128
|
+
hasErrors = true;
|
|
129
|
+
} else if (CONSECUTIVE_HYPHENS.test(data.name)) {
|
|
130
|
+
diagnostics.push({
|
|
131
|
+
ruleId: "name-format",
|
|
132
|
+
severity: "error",
|
|
133
|
+
message: `Skill name "${data.name}" contains consecutive hyphens (--), which is not allowed`,
|
|
134
|
+
file: filePath,
|
|
135
|
+
line: 1,
|
|
136
|
+
fix: `Replace consecutive hyphens with single hyphens: ${data.name.replace(/--+/g, "-")}`
|
|
137
|
+
});
|
|
138
|
+
hasErrors = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (data.description != null && typeof data.description !== "string") {
|
|
142
|
+
diagnostics.push({
|
|
143
|
+
ruleId: "description-type",
|
|
144
|
+
severity: "error",
|
|
145
|
+
message: 'Frontmatter "description" field must be a string',
|
|
146
|
+
file: filePath,
|
|
147
|
+
line: 1,
|
|
148
|
+
fix: 'Ensure the description is a string: description: "A clear description"'
|
|
149
|
+
});
|
|
150
|
+
hasErrors = true;
|
|
151
|
+
} else if (data.description == null || typeof data.description === "string" && data.description.trim().length === 0) {
|
|
152
|
+
diagnostics.push({
|
|
153
|
+
ruleId: "description-required",
|
|
154
|
+
severity: "error",
|
|
155
|
+
message: 'Frontmatter must contain a "description" field (required by spec)',
|
|
156
|
+
file: filePath,
|
|
157
|
+
line: 1,
|
|
158
|
+
fix: 'Add a description: description: "What this skill does and when to use it"'
|
|
159
|
+
});
|
|
160
|
+
hasErrors = true;
|
|
161
|
+
} else if (typeof data.description === "string") {
|
|
162
|
+
const descLen = data.description.length;
|
|
163
|
+
if (descLen < 10) {
|
|
164
|
+
diagnostics.push({
|
|
165
|
+
ruleId: "description-length",
|
|
166
|
+
severity: "warning",
|
|
167
|
+
message: `Description is too short (${descLen} chars). Should be at least 50 characters for effective agent routing`,
|
|
168
|
+
file: filePath,
|
|
169
|
+
line: 1,
|
|
170
|
+
fix: "Expand the description to explain what the skill does, when to use it, and what triggers it"
|
|
171
|
+
});
|
|
172
|
+
} else if (descLen > 1024) {
|
|
173
|
+
diagnostics.push({
|
|
174
|
+
ruleId: "description-length",
|
|
175
|
+
severity: "error",
|
|
176
|
+
message: `Description exceeds max length (${descLen} chars). Must be at most 1,024 characters per spec`,
|
|
177
|
+
file: filePath,
|
|
178
|
+
line: 1,
|
|
179
|
+
fix: "Shorten the description to the essential information. Move details to the instructions body"
|
|
180
|
+
});
|
|
181
|
+
hasErrors = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const body = parsed.content.trim();
|
|
185
|
+
if (body.length === 0) {
|
|
186
|
+
diagnostics.push({
|
|
187
|
+
ruleId: "body-required",
|
|
188
|
+
severity: "error",
|
|
189
|
+
message: "SKILL.md must have markdown content after the frontmatter",
|
|
190
|
+
file: filePath,
|
|
191
|
+
fix: "Add instructions below the frontmatter that teach an agent how to use this skill"
|
|
192
|
+
});
|
|
193
|
+
hasErrors = true;
|
|
194
|
+
}
|
|
195
|
+
const sections = parseSections(parsed.content);
|
|
196
|
+
const fileReferences = extractFileReferences(parsed.content, filePath, dirPath);
|
|
197
|
+
for (const ref of fileReferences) {
|
|
198
|
+
if (!ref.exists) {
|
|
199
|
+
diagnostics.push({
|
|
200
|
+
ruleId: "file-reference-exists",
|
|
201
|
+
severity: "error",
|
|
202
|
+
message: `Referenced file not found: ${ref.path}`,
|
|
203
|
+
file: filePath,
|
|
204
|
+
line: ref.line,
|
|
205
|
+
fix: `Create the file at ${ref.path} or remove the reference`
|
|
206
|
+
});
|
|
207
|
+
hasErrors = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const tokenCount = countTokens(rawContent);
|
|
211
|
+
if (tokenCount > 5e3) {
|
|
212
|
+
diagnostics.push({
|
|
213
|
+
ruleId: "token-budget",
|
|
214
|
+
severity: "warning",
|
|
215
|
+
message: `Token count (${tokenCount}) exceeds the recommended 5,000 token budget`,
|
|
216
|
+
file: filePath,
|
|
217
|
+
fix: "Move detailed content to references/ directory to keep the main SKILL.md lean"
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const knownFields = ["name", "description", "version"];
|
|
221
|
+
const dangerousKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
222
|
+
const metadata = {
|
|
223
|
+
...typeof data.name === "string" ? { name: data.name } : {},
|
|
224
|
+
...typeof data.description === "string" ? { description: data.description } : {},
|
|
225
|
+
...data.version != null ? { version: String(data.version) } : {},
|
|
226
|
+
...Object.fromEntries(
|
|
227
|
+
Object.entries(data).filter(([k]) => !knownFields.includes(k) && !dangerousKeys.has(k))
|
|
228
|
+
)
|
|
229
|
+
};
|
|
230
|
+
if (hasErrors) {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
skill: null,
|
|
234
|
+
diagnostics
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const lineCount = body.split("\n").length;
|
|
238
|
+
const skill = {
|
|
239
|
+
metadata,
|
|
240
|
+
body,
|
|
241
|
+
sections,
|
|
242
|
+
fileReferences,
|
|
243
|
+
filePath,
|
|
244
|
+
dirPath,
|
|
245
|
+
tokenCount,
|
|
246
|
+
lineCount,
|
|
247
|
+
rawContent
|
|
248
|
+
};
|
|
249
|
+
return { ok: true, skill, diagnostics };
|
|
250
|
+
}
|
|
251
|
+
function parseSections(content, _filePath) {
|
|
252
|
+
const lines = content.split("\n");
|
|
253
|
+
const sections = [];
|
|
254
|
+
let currentSection = null;
|
|
255
|
+
const frontmatterOffset = 0;
|
|
256
|
+
for (let i = 0; i < lines.length; i++) {
|
|
257
|
+
const line = lines[i];
|
|
258
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
259
|
+
if (headingMatch) {
|
|
260
|
+
if (currentSection) {
|
|
261
|
+
sections.push({
|
|
262
|
+
heading: currentSection.heading,
|
|
263
|
+
depth: currentSection.depth,
|
|
264
|
+
content: currentSection.lines.join("\n").trim(),
|
|
265
|
+
line: currentSection.line
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
currentSection = {
|
|
269
|
+
heading: headingMatch[2],
|
|
270
|
+
depth: headingMatch[1].length,
|
|
271
|
+
line: frontmatterOffset + i + 1,
|
|
272
|
+
lines: []
|
|
273
|
+
};
|
|
274
|
+
} else if (currentSection) {
|
|
275
|
+
currentSection.lines.push(line);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (currentSection) {
|
|
279
|
+
sections.push({
|
|
280
|
+
heading: currentSection.heading,
|
|
281
|
+
depth: currentSection.depth,
|
|
282
|
+
content: currentSection.lines.join("\n").trim(),
|
|
283
|
+
line: currentSection.line
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return sections;
|
|
287
|
+
}
|
|
288
|
+
function extractFileReferences(content, _filePath, dirPath) {
|
|
289
|
+
const references = [];
|
|
290
|
+
const seen = /* @__PURE__ */ new Set();
|
|
291
|
+
const lines = content.split("\n");
|
|
292
|
+
for (let i = 0; i < lines.length; i++) {
|
|
293
|
+
const line = lines[i];
|
|
294
|
+
for (const m of line.matchAll(FILE_REFERENCE_PATTERN)) {
|
|
295
|
+
const refPath = m[1];
|
|
296
|
+
if (seen.has(refPath)) continue;
|
|
297
|
+
seen.add(refPath);
|
|
298
|
+
const absoluteRefPath = resolve(dirPath, refPath);
|
|
299
|
+
if (!absoluteRefPath.startsWith(`${resolve(dirPath)}/`)) continue;
|
|
300
|
+
references.push({
|
|
301
|
+
path: refPath,
|
|
302
|
+
line: i + 1,
|
|
303
|
+
exists: existsSync(absoluteRefPath)
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
for (const m of line.matchAll(MARKDOWN_LINK_PATTERN)) {
|
|
307
|
+
const refPath = m[2];
|
|
308
|
+
if (refPath.startsWith("#") || refPath.startsWith("data:") || seen.has(refPath)) continue;
|
|
309
|
+
seen.add(refPath);
|
|
310
|
+
const absoluteRefPath = resolve(dirPath, refPath);
|
|
311
|
+
if (!absoluteRefPath.startsWith(`${resolve(dirPath)}/`)) continue;
|
|
312
|
+
references.push({
|
|
313
|
+
path: refPath,
|
|
314
|
+
line: i + 1,
|
|
315
|
+
exists: existsSync(absoluteRefPath)
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return references;
|
|
320
|
+
}
|
|
321
|
+
async function resolveSkillFiles(searchPath) {
|
|
322
|
+
const absolutePath = resolve(searchPath);
|
|
323
|
+
const locations = [];
|
|
324
|
+
const pathStat = await stat(absolutePath).catch(() => null);
|
|
325
|
+
if (!pathStat) {
|
|
326
|
+
return locations;
|
|
327
|
+
}
|
|
328
|
+
if (pathStat.isFile() && basename(absolutePath) === "SKILL.md") {
|
|
329
|
+
const directory = resolve(absolutePath, "..");
|
|
330
|
+
locations.push({
|
|
331
|
+
skillFile: absolutePath,
|
|
332
|
+
directory,
|
|
333
|
+
dirName: basename(directory)
|
|
334
|
+
});
|
|
335
|
+
return locations;
|
|
336
|
+
}
|
|
337
|
+
if (!pathStat.isDirectory()) {
|
|
338
|
+
return locations;
|
|
339
|
+
}
|
|
340
|
+
const directSkill = join(absolutePath, "SKILL.md");
|
|
341
|
+
const directStat = await stat(directSkill).catch(() => null);
|
|
342
|
+
if (directStat?.isFile()) {
|
|
343
|
+
locations.push({
|
|
344
|
+
skillFile: directSkill,
|
|
345
|
+
directory: absolutePath,
|
|
346
|
+
dirName: basename(absolutePath)
|
|
347
|
+
});
|
|
348
|
+
return locations;
|
|
349
|
+
}
|
|
350
|
+
const entries = await readdir(absolutePath, { withFileTypes: true });
|
|
351
|
+
const subdirChecks = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map(async (entry) => {
|
|
352
|
+
const subdir = join(absolutePath, entry.name);
|
|
353
|
+
const skillFile = join(subdir, "SKILL.md");
|
|
354
|
+
const skillStat = await stat(skillFile).catch(() => null);
|
|
355
|
+
if (skillStat?.isFile()) {
|
|
356
|
+
locations.push({
|
|
357
|
+
skillFile,
|
|
358
|
+
directory: subdir,
|
|
359
|
+
dirName: entry.name
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
await Promise.all(subdirChecks);
|
|
364
|
+
locations.sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
365
|
+
return locations;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export { countTokens, parseSkill, parseSkillContent, resolveSkillFiles };
|
|
369
|
+
//# sourceMappingURL=index.js.map
|
|
370
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tokenizer.ts","../src/parser.ts","../src/resolver.ts"],"names":["resolve"],"mappings":"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","file":"index.js","sourcesContent":["import { encodingForModel } from 'js-tiktoken';\n\n/**\n * Cached encoder instance. Created lazily on first use.\n * Uses cl100k_base encoding (GPT-4 / Claude-compatible).\n */\nlet encoder: ReturnType<typeof encodingForModel> | null = null;\n\n/**\n * Returns the shared tiktoken encoder instance.\n * Uses cl100k_base which is compatible with both OpenAI and Anthropic models.\n */\nfunction getEncoder(): ReturnType<typeof encodingForModel> {\n\tif (!encoder) {\n\t\tencoder = encodingForModel('gpt-4o');\n\t}\n\treturn encoder;\n}\n\n/**\n * Count the number of tokens in a string.\n *\n * Uses the cl100k_base tokenizer (GPT-4o compatible), which provides\n * a reasonable approximation for both OpenAI and Anthropic models.\n * Actual token counts may vary slightly between providers.\n *\n * @param text - The text to count tokens for\n * @returns The number of tokens\n *\n * @example\n * ```ts\n * const count = countTokens('Hello, world!');\n * // => 4\n * ```\n */\nexport function countTokens(text: string): number {\n\tif (text.length === 0) {\n\t\treturn 0;\n\t}\n\tconst enc = getEncoder();\n\treturn enc.encode(text).length;\n}\n","import { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, resolve } from 'node:path';\nimport matter from 'gray-matter';\nimport { countTokens } from './tokenizer.js';\nimport type {\n\tDiagnostic,\n\tParseResult,\n\tSkill,\n\tSkillFileReference,\n\tSkillMetadata,\n\tSkillSection,\n} from './types.js';\n\n/**\n * Pattern for matching file references in markdown content.\n * Matches paths like `scripts/foo.sh`, `references/bar.md`, `assets/img.png`.\n * Intentionally narrow to avoid false positives on URLs or code snippets.\n */\nconst FILE_REFERENCE_PATTERN = /(?:^|\\s|`)((?:scripts|references|assets)\\/[\\w./-]+(?:\\.\\w+)?)/gm;\n\n/**\n * Pattern for matching markdown links to local files.\n * Matches [text](path.ext) where path doesn't start with http:// or https://\n */\nconst MARKDOWN_LINK_PATTERN = /\\[([^\\]]*)\\]\\((?!https?:\\/\\/)([^)]+)\\)/g;\n\n/**\n * Pattern for the skill name: lowercase letters, numbers, and hyphens, max 64 chars.\n * Per the Agent Skills spec (agentskills.io):\n * - 1-64 characters\n * - Lowercase alphanumeric and hyphens only\n * - Must not start or end with a hyphen\n * - Must not contain consecutive hyphens (--)\n */\nconst NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]?$/;\nconst CONSECUTIVE_HYPHENS = /--/;\n\n/**\n * Parse a SKILL.md file from a file path.\n *\n * Reads the file, parses frontmatter and markdown body, extracts sections,\n * resolves file references, and counts tokens. Returns either a successful\n * parse result with the Skill object, or a failed result with diagnostics\n * explaining what went wrong.\n *\n * @param filePath - Absolute or relative path to a SKILL.md file\n * @returns ParseResult with either a Skill object or error diagnostics\n *\n * @example\n * ```ts\n * const result = await parseSkill('./my-skill/SKILL.md');\n * if (result.ok) {\n * console.log(result.skill.metadata.name);\n * } else {\n * console.error(result.diagnostics);\n * }\n * ```\n */\nexport async function parseSkill(filePath: string): Promise<ParseResult> {\n\tconst absolutePath = isAbsolute(filePath) ? filePath : resolve(filePath);\n\tconst dirPath = dirname(absolutePath);\n\n\t// Read the file\n\tlet rawContent: string;\n\ttry {\n\t\trawContent = await readFile(absolutePath, 'utf-8');\n\t} catch (err) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tskill: null,\n\t\t\tdiagnostics: [\n\t\t\t\t{\n\t\t\t\t\truleId: 'file-readable',\n\t\t\t\t\tseverity: 'error',\n\t\t\t\t\tmessage: `Cannot read file: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t\t\tfile: absolutePath,\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t}\n\n\t// Check for empty file\n\tif (rawContent.trim().length === 0) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tskill: null,\n\t\t\tdiagnostics: [\n\t\t\t\t{\n\t\t\t\t\truleId: 'file-not-empty',\n\t\t\t\t\tseverity: 'error',\n\t\t\t\t\tmessage: 'SKILL.md file is empty',\n\t\t\t\t\tfile: absolutePath,\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t}\n\n\treturn parseSkillContent(rawContent, absolutePath, dirPath);\n}\n\n/**\n * Parse SKILL.md content from a raw string.\n * Useful when you already have the content in memory.\n *\n * @param rawContent - The raw SKILL.md file content\n * @param filePath - The file path (used for diagnostics and file reference resolution)\n * @param dirPath - The skill directory path (used for file reference resolution)\n * @returns ParseResult with either a Skill object or error diagnostics\n */\nexport function parseSkillContent(\n\trawContent: string,\n\tfilePath: string,\n\tdirPath: string,\n): ParseResult {\n\tconst diagnostics: Diagnostic[] = [];\n\tlet hasErrors = false;\n\n\t// Parse frontmatter\n\tlet parsed: matter.GrayMatterFile<string>;\n\ttry {\n\t\tparsed = matter(rawContent);\n\t} catch (err) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tskill: null,\n\t\t\tdiagnostics: [\n\t\t\t\t{\n\t\t\t\t\truleId: 'frontmatter-valid-yaml',\n\t\t\t\t\tseverity: 'error',\n\t\t\t\t\tmessage: `Invalid YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t\t\tfile: filePath,\n\t\t\t\t\tline: 1,\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t}\n\n\t// Check frontmatter existence — use raw content check since gray-matter's\n\t// .matter property can be unreliable across successive calls\n\tconst hasFrontmatter =\n\t\trawContent.trimStart().startsWith('---') &&\n\t\tObject.keys(parsed.data as Record<string, unknown>).length > 0;\n\tif (!hasFrontmatter) {\n\t\tdiagnostics.push({\n\t\t\truleId: 'frontmatter-required',\n\t\t\tseverity: 'error',\n\t\t\tmessage: 'SKILL.md must have YAML frontmatter between --- delimiters',\n\t\t\tfile: filePath,\n\t\t\tline: 1,\n\t\t\tfix: 'Add frontmatter at the top of the file:\\n---\\nname: my-skill\\ndescription: A short description\\n---',\n\t\t});\n\t\thasErrors = true;\n\t}\n\n\t// Validate metadata fields\n\tconst data = parsed.data as Record<string, unknown>;\n\n\t// Validate name (REQUIRED per agentskills.io spec)\n\tif (data.name != null && typeof data.name !== 'string') {\n\t\tdiagnostics.push({\n\t\t\truleId: 'name-type',\n\t\t\tseverity: 'error',\n\t\t\tmessage: 'Frontmatter \"name\" field must be a string',\n\t\t\tfile: filePath,\n\t\t\tline: 1,\n\t\t\tfix: 'Ensure the name is a string: name: my-skill-name',\n\t\t});\n\t\thasErrors = true;\n\t} else if (\n\t\tdata.name == null ||\n\t\t(typeof data.name === 'string' && data.name.trim().length === 0)\n\t) {\n\t\tdiagnostics.push({\n\t\t\truleId: 'name-required',\n\t\t\tseverity: 'error',\n\t\t\tmessage: 'Frontmatter must contain a \"name\" field (required by spec)',\n\t\t\tfile: filePath,\n\t\t\tline: 1,\n\t\t\tfix: 'Add a name field: name: my-skill-name',\n\t\t});\n\t\thasErrors = true;\n\t} else if (typeof data.name === 'string') {\n\t\tif (!NAME_PATTERN.test(data.name)) {\n\t\t\tdiagnostics.push({\n\t\t\t\truleId: 'name-format',\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: `Skill name \"${data.name}\" is invalid. Must be 1-64 chars, lowercase letters, numbers, and hyphens only. Must not start or end with a hyphen`,\n\t\t\t\tfile: filePath,\n\t\t\t\tline: 1,\n\t\t\t\tfix: `Use a name like: ${String(data.name)\n\t\t\t\t\t.toLowerCase()\n\t\t\t\t\t.replace(/[^a-z0-9-]/g, '-')\n\t\t\t\t\t.replace(/-+/g, '-')\n\t\t\t\t\t.replace(/^-|-$/g, '')}`,\n\t\t\t});\n\t\t\thasErrors = true;\n\t\t} else if (CONSECUTIVE_HYPHENS.test(data.name)) {\n\t\t\tdiagnostics.push({\n\t\t\t\truleId: 'name-format',\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: `Skill name \"${data.name}\" contains consecutive hyphens (--), which is not allowed`,\n\t\t\t\tfile: filePath,\n\t\t\t\tline: 1,\n\t\t\t\tfix: `Replace consecutive hyphens with single hyphens: ${data.name.replace(/--+/g, '-')}`,\n\t\t\t});\n\t\t\thasErrors = true;\n\t\t}\n\t}\n\n\t// Validate description (REQUIRED per agentskills.io spec)\n\tif (data.description != null && typeof data.description !== 'string') {\n\t\tdiagnostics.push({\n\t\t\truleId: 'description-type',\n\t\t\tseverity: 'error',\n\t\t\tmessage: 'Frontmatter \"description\" field must be a string',\n\t\t\tfile: filePath,\n\t\t\tline: 1,\n\t\t\tfix: 'Ensure the description is a string: description: \"A clear description\"',\n\t\t});\n\t\thasErrors = true;\n\t} else if (\n\t\tdata.description == null ||\n\t\t(typeof data.description === 'string' && data.description.trim().length === 0)\n\t) {\n\t\tdiagnostics.push({\n\t\t\truleId: 'description-required',\n\t\t\tseverity: 'error',\n\t\t\tmessage: 'Frontmatter must contain a \"description\" field (required by spec)',\n\t\t\tfile: filePath,\n\t\t\tline: 1,\n\t\t\tfix: 'Add a description: description: \"What this skill does and when to use it\"',\n\t\t});\n\t\thasErrors = true;\n\t} else if (typeof data.description === 'string') {\n\t\tconst descLen = data.description.length;\n\t\tif (descLen < 10) {\n\t\t\tdiagnostics.push({\n\t\t\t\truleId: 'description-length',\n\t\t\t\tseverity: 'warning',\n\t\t\t\tmessage: `Description is too short (${descLen} chars). Should be at least 50 characters for effective agent routing`,\n\t\t\t\tfile: filePath,\n\t\t\t\tline: 1,\n\t\t\t\tfix: 'Expand the description to explain what the skill does, when to use it, and what triggers it',\n\t\t\t});\n\t\t} else if (descLen > 1024) {\n\t\t\tdiagnostics.push({\n\t\t\t\truleId: 'description-length',\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: `Description exceeds max length (${descLen} chars). Must be at most 1,024 characters per spec`,\n\t\t\t\tfile: filePath,\n\t\t\t\tline: 1,\n\t\t\t\tfix: 'Shorten the description to the essential information. Move details to the instructions body',\n\t\t\t});\n\t\t\thasErrors = true;\n\t\t}\n\t}\n\n\t// Check markdown body\n\tconst body = parsed.content.trim();\n\tif (body.length === 0) {\n\t\tdiagnostics.push({\n\t\t\truleId: 'body-required',\n\t\t\tseverity: 'error',\n\t\t\tmessage: 'SKILL.md must have markdown content after the frontmatter',\n\t\t\tfile: filePath,\n\t\t\tfix: 'Add instructions below the frontmatter that teach an agent how to use this skill',\n\t\t});\n\t\thasErrors = true;\n\t}\n\n\t// Parse sections\n\tconst sections = parseSections(parsed.content, filePath);\n\n\t// Extract file references\n\tconst fileReferences = extractFileReferences(parsed.content, filePath, dirPath);\n\n\t// Check for missing referenced files\n\tfor (const ref of fileReferences) {\n\t\tif (!ref.exists) {\n\t\t\tdiagnostics.push({\n\t\t\t\truleId: 'file-reference-exists',\n\t\t\t\tseverity: 'error',\n\t\t\t\tmessage: `Referenced file not found: ${ref.path}`,\n\t\t\t\tfile: filePath,\n\t\t\t\tline: ref.line,\n\t\t\t\tfix: `Create the file at ${ref.path} or remove the reference`,\n\t\t\t});\n\t\t\thasErrors = true;\n\t\t}\n\t}\n\n\t// Count tokens\n\tconst tokenCount = countTokens(rawContent);\n\tif (tokenCount > 5000) {\n\t\tdiagnostics.push({\n\t\t\truleId: 'token-budget',\n\t\t\tseverity: 'warning',\n\t\t\tmessage: `Token count (${tokenCount}) exceeds the recommended 5,000 token budget`,\n\t\t\tfile: filePath,\n\t\t\tfix: 'Move detailed content to references/ directory to keep the main SKILL.md lean',\n\t\t});\n\t}\n\n\t// Build metadata from all frontmatter fields\n\tconst knownFields = ['name', 'description', 'version'];\n\tconst dangerousKeys = new Set(['__proto__', 'constructor', 'prototype']);\n\tconst metadata: SkillMetadata = {\n\t\t...(typeof data.name === 'string' ? { name: data.name } : {}),\n\t\t...(typeof data.description === 'string' ? { description: data.description } : {}),\n\t\t...(data.version != null ? { version: String(data.version) } : {}),\n\t\t...Object.fromEntries(\n\t\t\tObject.entries(data).filter(([k]) => !knownFields.includes(k) && !dangerousKeys.has(k)),\n\t\t),\n\t};\n\n\tif (hasErrors) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tskill: null,\n\t\t\tdiagnostics,\n\t\t};\n\t}\n\n\tconst lineCount = body.split('\\n').length;\n\n\tconst skill: Skill = {\n\t\tmetadata,\n\t\tbody,\n\t\tsections,\n\t\tfileReferences,\n\t\tfilePath,\n\t\tdirPath,\n\t\ttokenCount,\n\t\tlineCount,\n\t\trawContent,\n\t};\n\n\treturn { ok: true, skill, diagnostics };\n}\n\n/**\n * Parse the markdown body into sections based on headings.\n */\nfunction parseSections(content: string, _filePath: string): SkillSection[] {\n\tconst lines = content.split('\\n');\n\tconst sections: SkillSection[] = [];\n\tlet currentSection: { heading: string; depth: number; line: number; lines: string[] } | null =\n\t\tnull;\n\n\t// Calculate the frontmatter offset (lines before content)\n\t// gray-matter strips the frontmatter, so we need to count from line 1 of the content\n\tconst frontmatterOffset = 0; // Sections are relative to the content start\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i]!;\n\t\tconst headingMatch = line.match(/^(#{1,6})\\s+(.+)$/);\n\n\t\tif (headingMatch) {\n\t\t\t// Save previous section\n\t\t\tif (currentSection) {\n\t\t\t\tsections.push({\n\t\t\t\t\theading: currentSection.heading,\n\t\t\t\t\tdepth: currentSection.depth,\n\t\t\t\t\tcontent: currentSection.lines.join('\\n').trim(),\n\t\t\t\t\tline: currentSection.line,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tcurrentSection = {\n\t\t\t\theading: headingMatch[2]!,\n\t\t\t\tdepth: headingMatch[1]!.length,\n\t\t\t\tline: frontmatterOffset + i + 1,\n\t\t\t\tlines: [],\n\t\t\t};\n\t\t} else if (currentSection) {\n\t\t\tcurrentSection.lines.push(line);\n\t\t}\n\t}\n\n\t// Don't forget the last section\n\tif (currentSection) {\n\t\tsections.push({\n\t\t\theading: currentSection.heading,\n\t\t\tdepth: currentSection.depth,\n\t\t\tcontent: currentSection.lines.join('\\n').trim(),\n\t\t\tline: currentSection.line,\n\t\t});\n\t}\n\n\treturn sections;\n}\n\n/**\n * Extract file references from the markdown body.\n * Looks for paths starting with scripts/, references/, or assets/.\n */\nfunction extractFileReferences(\n\tcontent: string,\n\t_filePath: string,\n\tdirPath: string,\n): SkillFileReference[] {\n\tconst references: SkillFileReference[] = [];\n\tconst seen = new Set<string>();\n\tconst lines = content.split('\\n');\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i]!;\n\t\t// Match directory-prefixed paths: scripts/, references/, assets/\n\t\tfor (const m of line.matchAll(FILE_REFERENCE_PATTERN)) {\n\t\t\tconst refPath = m[1]!;\n\t\t\tif (seen.has(refPath)) continue;\n\t\t\tseen.add(refPath);\n\n\t\t\tconst absoluteRefPath = resolve(dirPath, refPath);\n\t\t\t// Guard against path traversal — reference must stay within skill directory\n\t\t\tif (!absoluteRefPath.startsWith(`${resolve(dirPath)}/`)) continue;\n\t\t\treferences.push({\n\t\t\t\tpath: refPath,\n\t\t\t\tline: i + 1,\n\t\t\t\texists: existsSync(absoluteRefPath),\n\t\t\t});\n\t\t}\n\n\t\t// Match markdown links to local files: [text](local-file.md)\n\t\tfor (const m of line.matchAll(MARKDOWN_LINK_PATTERN)) {\n\t\t\tconst refPath = m[2]!;\n\t\t\t// Skip anchors, data URIs, and already-seen paths\n\t\t\tif (refPath.startsWith('#') || refPath.startsWith('data:') || seen.has(refPath)) continue;\n\t\t\tseen.add(refPath);\n\n\t\t\tconst absoluteRefPath = resolve(dirPath, refPath);\n\t\t\t// Guard against path traversal — reference must stay within skill directory\n\t\t\tif (!absoluteRefPath.startsWith(`${resolve(dirPath)}/`)) continue;\n\t\t\treferences.push({\n\t\t\t\tpath: refPath,\n\t\t\t\tline: i + 1,\n\t\t\t\texists: existsSync(absoluteRefPath),\n\t\t\t});\n\t\t}\n\t}\n\n\treturn references;\n}\n","import { readdir, stat } from 'node:fs/promises';\nimport { basename, join, resolve } from 'node:path';\n\n/**\n * Information about a discovered skill in the filesystem.\n */\nexport interface SkillLocation {\n\t/** Absolute path to the SKILL.md file */\n\treadonly skillFile: string;\n\t/** Absolute path to the skill directory */\n\treadonly directory: string;\n\t/** The directory name (used as a fallback identifier) */\n\treadonly dirName: string;\n}\n\n/**\n * Resolve all SKILL.md files in a directory tree.\n *\n * Searches for SKILL.md files in the given path. If the path itself\n * contains a SKILL.md, returns just that one. If the path is a directory,\n * searches one level deep for subdirectories containing SKILL.md files.\n *\n * @param searchPath - Absolute or relative path to search\n * @returns Array of discovered skill locations\n *\n * @example\n * ```ts\n * // Single skill directory\n * const skills = await resolveSkillFiles('./my-skill/');\n * // => [{ skillFile: '/abs/path/my-skill/SKILL.md', ... }]\n *\n * // Directory of skills\n * const skills = await resolveSkillFiles('./skills/');\n * // => [\n * // { skillFile: '/abs/path/skills/deploy/SKILL.md', ... },\n * // { skillFile: '/abs/path/skills/test-runner/SKILL.md', ... },\n * // ]\n * ```\n */\nexport async function resolveSkillFiles(searchPath: string): Promise<SkillLocation[]> {\n\tconst absolutePath = resolve(searchPath);\n\tconst locations: SkillLocation[] = [];\n\n\t// Check if the path itself is a SKILL.md file\n\tconst pathStat = await stat(absolutePath).catch(() => null);\n\tif (!pathStat) {\n\t\treturn locations;\n\t}\n\n\tif (pathStat.isFile() && basename(absolutePath) === 'SKILL.md') {\n\t\tconst directory = resolve(absolutePath, '..');\n\t\tlocations.push({\n\t\t\tskillFile: absolutePath,\n\t\t\tdirectory,\n\t\t\tdirName: basename(directory),\n\t\t});\n\t\treturn locations;\n\t}\n\n\tif (!pathStat.isDirectory()) {\n\t\treturn locations;\n\t}\n\n\t// Check if this directory contains a SKILL.md\n\tconst directSkill = join(absolutePath, 'SKILL.md');\n\tconst directStat = await stat(directSkill).catch(() => null);\n\tif (directStat?.isFile()) {\n\t\tlocations.push({\n\t\t\tskillFile: directSkill,\n\t\t\tdirectory: absolutePath,\n\t\t\tdirName: basename(absolutePath),\n\t\t});\n\t\treturn locations;\n\t}\n\n\t// Search one level deep for subdirectories with SKILL.md\n\tconst entries = await readdir(absolutePath, { withFileTypes: true });\n\tconst subdirChecks = entries\n\t\t.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))\n\t\t.map(async (entry) => {\n\t\t\tconst subdir = join(absolutePath, entry.name);\n\t\t\tconst skillFile = join(subdir, 'SKILL.md');\n\t\t\tconst skillStat = await stat(skillFile).catch(() => null);\n\t\t\tif (skillStat?.isFile()) {\n\t\t\t\tlocations.push({\n\t\t\t\t\tskillFile,\n\t\t\t\t\tdirectory: subdir,\n\t\t\t\t\tdirName: entry.name,\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\n\tawait Promise.all(subdirChecks);\n\n\t// Sort by directory name for consistent ordering\n\tlocations.sort((a, b) => a.dirName.localeCompare(b.dirName));\n\n\treturn locations;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@skill-tools/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Core parser, types, and utilities for Agent Skills (SKILL.md)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"agent-skills",
|
|
26
|
+
"skill-md",
|
|
27
|
+
"parser",
|
|
28
|
+
"validation",
|
|
29
|
+
"ai-agent"
|
|
30
|
+
],
|
|
31
|
+
"author": "Piyush Vyas <pyyush>",
|
|
32
|
+
"license": "Apache-2.0",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/skill-tools/skill-tools"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"gray-matter": "^4.0.3",
|
|
42
|
+
"js-tiktoken": "^1.0.18"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@biomejs/biome": "^2.3.14",
|
|
46
|
+
"@types/node": "^25.2.2",
|
|
47
|
+
"tsup": "^8.5.1",
|
|
48
|
+
"typescript": "^5.9.3",
|
|
49
|
+
"vitest": "^4.0.18"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup",
|
|
53
|
+
"dev": "tsup --watch",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:watch": "vitest",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"lint": "biome check src/",
|
|
58
|
+
"lint:fix": "biome check --write src/",
|
|
59
|
+
"format": "biome format --write src/",
|
|
60
|
+
"format:check": "biome format src/",
|
|
61
|
+
"clean": "rm -rf dist"
|
|
62
|
+
}
|
|
63
|
+
}
|