-
Notifications
You must be signed in to change notification settings - Fork 198
Expand file tree
/
Copy pathskill-helper.ts
More file actions
144 lines (123 loc) · 4.82 KB
/
Copy pathskill-helper.ts
File metadata and controls
144 lines (123 loc) · 4.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/**
* Skill Utility
*
* Shared helpers for loading, listing, and parsing SKILL.md files from the
* plugin/skills directory. All frontmatter parsing goes through
* `parseSkillContent` which normalises line endings, validates `---`
* delimiters, and exposes the raw YAML source.
*/
import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import matter from "gray-matter";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// ── Types ────────────────────────────────────────────────────────────────────
/** Parsed frontmatter result from a SKILL.md file. */
export interface ParsedSkill {
/** Parsed key-value pairs (name, description, …). */
data: Record<string, unknown>;
/** Markdown body after the closing `---`. */
content: string;
/**
* Raw frontmatter text between the `---` delimiters (not including the
* delimiters themselves). Useful for checks that need the original YAML
* source — e.g. detecting block scalars (`>-`, `|`) or XML-like tags.
*/
raw: string;
}
interface SkillMetadata {
name: string;
description: string;
[key: string]: unknown;
}
export interface LoadedSkill {
metadata: SkillMetadata;
content: string;
path: string;
filePath: string;
}
// ── Parser ───────────────────────────────────────────────────────────────────
/**
* Parse SKILL.md file content and extract frontmatter.
*
* - Normalises `\r\n` → `\n` before parsing.
* - Returns `null` when the file does not contain valid `---` delimited
* frontmatter (instead of throwing).
*/
export function parseSkillContent(fileContent: string): ParsedSkill | null {
// Normalise Windows line-endings
const normalised = fileContent.replace(/\r\n/g, "\n");
// Quick guard: gray-matter is lenient — we require the file to start
// with `---` (the agentskills.io spec mandates it).
if (!normalised.startsWith("---")) return null;
// Also require a closing `---` delimiter. gray-matter treats
// EOF as an implicit close, but the spec requires explicit delimiters.
const closingIndex = normalised.indexOf("\n---", 3);
if (closingIndex === -1) return null;
try {
const result = matter(normalised);
// gray-matter sets `data` to `{}` when there is no frontmatter or
// when the delimiters are malformed. Treat that as "no frontmatter".
if (Object.keys(result.data).length === 0) return null;
// Extract the raw YAML block. gray-matter exposes `result.matter` in
// recent versions but its behaviour across versions is inconsistent,
// so we derive it ourselves from the normalised input.
const raw = normalised.substring(4, closingIndex);
return {
data: result.data as Record<string, unknown>,
content: result.content,
raw,
};
} catch {
// YAML parse error → treat as "no valid frontmatter"
return null;
}
}
// ── Loaders ──────────────────────────────────────────────────────────────────
/**
* Load a skill by name.
*
* Reads the SKILL.md file from `plugin/skills/<skillName>` and parses it
* via `parseSkillContent`. Throws when the file is missing or contains
* no valid frontmatter.
*/
export function loadSkill(skillName: string): LoadedSkill {
const skillPath = path.join(
path.resolve(__dirname, "../../../plugin/skills"),
skillName
);
const skillFile = path.join(skillPath, "SKILL.md");
if (!fs.existsSync(skillFile)) {
throw new Error(`SKILL.md not found for skill: ${skillName} at ${skillFile}`);
}
const fileContent = fs.readFileSync(skillFile, "utf-8");
const parsed = parseSkillContent(fileContent);
if (!parsed) {
throw new Error(`Invalid or missing frontmatter in SKILL.md for skill: ${skillName}`);
}
return {
metadata: {
name: (parsed.data.name as string) || skillName,
description: (parsed.data.description as string) || "",
...parsed.data
},
content: parsed.content.trim(),
path: skillPath,
filePath: skillFile
};
}
/**
* @returns Names of skills in azure plugin.
*/
export function listSkills(): string[] {
const skillsDir = path.resolve(__dirname, "../../../plugin/skills");
const items = fs.readdirSync(skillsDir, { withFileTypes: true });
return items
.filter((item) => item.isDirectory())
.filter((item) => {
const skillMdPath = path.join(skillsDir, item.name, "SKILL.md");
return fs.existsSync(skillMdPath);
})
.map((item) => item.name);
}