-
Notifications
You must be signed in to change notification settings - Fork 342
Expand file tree
/
Copy pathskill-loader.ts
More file actions
160 lines (135 loc) · 4.42 KB
/
Copy pathskill-loader.ts
File metadata and controls
160 lines (135 loc) · 4.42 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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import { readFileSync, existsSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import yaml from 'js-yaml';
import { warn } from './format.js';
/**
* Agent Skills standard frontmatter — matches agentskills.io spec exactly.
* gitagent-specific fields (category, risk_tier, etc.) live in metadata.
*/
export interface SkillFrontmatter {
name: string;
description: string;
license?: string;
compatibility?: string;
'allowed-tools'?: string;
metadata?: Record<string, string>;
}
export interface ParsedSkill {
frontmatter: SkillFrontmatter;
instructions: string;
directory: string;
hasScripts: boolean;
hasReferences: boolean;
hasAssets: boolean;
hasAgents: boolean;
}
export interface SkillMetadata {
name: string;
description: string;
license?: string;
allowedTools?: string[];
directory: string;
}
/**
* Parse a SKILL.md file into frontmatter + instructions body.
*/
export function parseSkillMd(filePath: string): ParsedSkill {
const content = readFileSync(filePath, 'utf-8');
const dir = join(filePath, '..');
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n*([\s\S]*)$/);
let frontmatter: SkillFrontmatter;
let instructions: string;
if (frontmatterMatch) {
frontmatter = yaml.load(frontmatterMatch[1]) as SkillFrontmatter;
instructions = frontmatterMatch[2].trim();
} else {
throw new Error(`SKILL.md at ${filePath} is missing YAML frontmatter (---)`);
}
if (!frontmatter.name || !frontmatter.description) {
throw new Error(`SKILL.md at ${filePath} is missing required fields: name, description`);
}
return {
frontmatter,
instructions,
directory: dir,
hasScripts: existsSync(join(dir, 'scripts')),
hasReferences: existsSync(join(dir, 'references')),
hasAssets: existsSync(join(dir, 'assets')),
hasAgents: existsSync(join(dir, 'agents')),
};
}
/**
* Progressive disclosure: load metadata only (~100 tokens).
* Returns name + description for lightweight listing/routing.
*/
export function loadSkillMetadata(filePath: string): SkillMetadata {
const content = readFileSync(filePath, 'utf-8');
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!frontmatterMatch) {
throw new Error(`SKILL.md at ${filePath} is missing YAML frontmatter`);
}
const fm = yaml.load(frontmatterMatch[1]) as SkillFrontmatter;
const tools = getAllowedTools(fm);
return {
name: fm.name,
description: fm.description,
license: fm.license,
allowedTools: tools.length > 0 ? tools : undefined,
directory: join(filePath, '..'),
};
}
/**
* Progressive disclosure: load full skill (<5000 tokens recommended).
* Returns complete frontmatter + instructions for active use.
*/
export function loadSkillFull(filePath: string): ParsedSkill {
return parseSkillMd(filePath);
}
/**
* Get the list of allowed tools from the space-delimited string.
*/
export function getAllowedTools(skill: SkillFrontmatter): string[] {
const tools = skill['allowed-tools'];
if (!tools || tools.trim() === '') return [];
return tools.split(/\s+/).filter(Boolean);
}
/**
* Load all skills from a skills/ directory.
* Returns array of fully parsed skills.
*/
export function loadAllSkills(skillsDir: string): ParsedSkill[] {
if (!existsSync(skillsDir)) return [];
const skills: ParsedSkill[] = [];
const entries = readdirSync(skillsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const skillMdPath = join(skillsDir, entry.name, 'SKILL.md');
if (!existsSync(skillMdPath)) continue;
try {
skills.push(parseSkillMd(skillMdPath));
} catch (err) {
warn(`Skill parse failed: ${skillMdPath} — ${(err as Error).message}`);
}
}
return skills;
}
/**
* Load all skill metadata from a skills/ directory.
* Lightweight version for listing/routing.
*/
export function loadAllSkillMetadata(skillsDir: string): SkillMetadata[] {
if (!existsSync(skillsDir)) return [];
const skills: SkillMetadata[] = [];
const entries = readdirSync(skillsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const skillMdPath = join(skillsDir, entry.name, 'SKILL.md');
if (!existsSync(skillMdPath)) continue;
try {
skills.push(loadSkillMetadata(skillMdPath));
} catch (err) {
warn(`Skill metadata load failed: ${skillMdPath} — ${(err as Error).message}`);
}
}
return skills;
}