load-skills 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/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # load-skills
2
+
3
+ `load-skills` loads skill content from configurable locations and validates skill spec constraints.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add load-skills
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { loadSkills } from "load-skills";
15
+
16
+ const { skills, report } = await loadSkills({
17
+ paths: ["./.agents/skills"],
18
+ recursive: false,
19
+ });
20
+ ```
21
+
22
+ `config` is optional. Calling `loadSkills()` uses:
23
+
24
+ - `paths: ["./.agents/skills"]`
25
+ - `recursive: false`
26
+
27
+ `skills` includes loaded skill payloads:
28
+
29
+ - `meta`: parsed frontmatter object
30
+ - `content`: `SKILL.md` content without frontmatter
31
+ - `references`: absolute file paths from `references/`
32
+ - `scripts`: script descriptors from `scripts/` as `{ path, type }`
33
+ - `state`: `"valid"` or `"invalid"`
34
+ - `warnings`: warning objects with machine-readable `code`
35
+
36
+ `report` includes one entry per configured path:
37
+
38
+ - `paths`: one entry per configured path:
39
+ - `inputPath`: raw path from config
40
+ - `resolvedPath`: absolute resolved path
41
+ - `count`: number of included skills from that path
42
+ - `skillNames`: included skill names from that path
43
+ - `error` (optional): `path_not_found` or `path_not_directory`
44
+ - `ignoredDuplicates`: map of skipped duplicate skills (first-find-wins), keyed by kept skill name
45
+
46
+ ## Path Priority
47
+
48
+ `paths` is also a priority list. If the same `meta.name` appears in multiple paths, the first one found is included and later ones are ignored.
49
+
50
+ ## Warning codes
51
+
52
+ - `missing_frontmatter`: `SKILL.md` is missing a valid `--- ... ---` frontmatter block.
53
+ - `invalid_yaml_frontmatter`: Frontmatter exists but YAML parsing failed or did not produce an object.
54
+ - `missing_required_meta_name`: Frontmatter is missing required `name`.
55
+ - `missing_required_meta_description`: Frontmatter is missing required `description`.
56
+ - `invalid_meta_name`: `name` is present but not a non-empty string.
57
+ - `invalid_meta_description`: `description` is present but not a non-empty string.
58
+ - `skill_md_content_size_limit_exceeded`: `SKILL.md` body exceeds 500 lines.
59
+ - `reference_large_without_toc`: A `references/` file exceeds 300 lines without a table-of-contents marker.
60
+ - `resource_read_error`: A resource file or directory could not be read during scanning.
@@ -0,0 +1,12 @@
1
+ import type { LoadSkillsConfig, LoadSkillsPathReport } from "./types.js";
2
+ export interface DiscoveredSkillFile {
3
+ skillFilePath: string;
4
+ pathReportIndex: number;
5
+ inputPath: string;
6
+ }
7
+ export interface DiscoveryResult {
8
+ discoveredSkillFiles: DiscoveredSkillFile[];
9
+ report: LoadSkillsPathReport[];
10
+ }
11
+ export declare function discoverSkillFiles(config: LoadSkillsConfig): Promise<DiscoveryResult>;
12
+ //# sourceMappingURL=discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGzE,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,oBAAoB,EAAE,mBAAmB,EAAE,CAAC;IAC5C,MAAM,EAAE,oBAAoB,EAAE,CAAC;CAChC;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,eAAe,CAAC,CA6D1B"}
@@ -0,0 +1,103 @@
1
+ import { constants } from "node:fs";
2
+ import { access, readdir, realpath, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { pathExists } from "./utils.js";
5
+ export async function discoverSkillFiles(config) {
6
+ const cwd = config.cwd ?? process.cwd();
7
+ const recursive = config.recursive ?? false;
8
+ const inputPaths = config.paths ?? ["./.agents/skills"];
9
+ const pathPairs = inputPaths.map((inputPath) => ({
10
+ inputPath,
11
+ resolvedPath: path.resolve(cwd, inputPath),
12
+ }));
13
+ const report = [];
14
+ const discoveredSkillFiles = [];
15
+ const discovered = new Set();
16
+ for (const pair of pathPairs) {
17
+ const rootPath = pair.resolvedPath;
18
+ const inputPath = pair.inputPath;
19
+ const reportItem = {
20
+ inputPath,
21
+ resolvedPath: rootPath,
22
+ count: 0,
23
+ skillNames: [],
24
+ };
25
+ const rootExists = await pathExists(rootPath);
26
+ if (!rootExists) {
27
+ reportItem.error = "path_not_found";
28
+ report.push(reportItem);
29
+ continue;
30
+ }
31
+ const rootStat = await stat(rootPath);
32
+ if (!rootStat.isDirectory()) {
33
+ reportItem.error = "path_not_directory";
34
+ report.push(reportItem);
35
+ continue;
36
+ }
37
+ const found = recursive
38
+ ? await discoverRecursive(rootPath)
39
+ : await discoverNonRecursive(rootPath);
40
+ const pathReportIndex = report.length;
41
+ for (const skillFilePath of found) {
42
+ const canonical = await realpath(skillFilePath);
43
+ if (!discovered.has(canonical)) {
44
+ discovered.add(canonical);
45
+ discoveredSkillFiles.push({
46
+ skillFilePath: canonical,
47
+ pathReportIndex,
48
+ inputPath,
49
+ });
50
+ }
51
+ }
52
+ report.push(reportItem);
53
+ }
54
+ return {
55
+ discoveredSkillFiles,
56
+ report,
57
+ };
58
+ }
59
+ async function discoverNonRecursive(rootPath) {
60
+ const entries = await readdir(rootPath, { withFileTypes: true });
61
+ const paths = [];
62
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
63
+ if (!entry.isDirectory()) {
64
+ continue;
65
+ }
66
+ const candidate = path.join(rootPath, entry.name, "SKILL.md");
67
+ if (await isReadableFile(candidate)) {
68
+ paths.push(candidate);
69
+ }
70
+ }
71
+ return paths;
72
+ }
73
+ async function discoverRecursive(rootPath) {
74
+ const paths = [];
75
+ async function walk(currentPath) {
76
+ const entries = await readdir(currentPath, { withFileTypes: true });
77
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
78
+ const fullPath = path.join(currentPath, entry.name);
79
+ if (entry.isFile() && entry.name === "SKILL.md") {
80
+ paths.push(fullPath);
81
+ continue;
82
+ }
83
+ if (entry.isDirectory()) {
84
+ await walk(fullPath);
85
+ }
86
+ }
87
+ }
88
+ await walk(rootPath);
89
+ return paths;
90
+ }
91
+ async function isReadableFile(targetPath) {
92
+ try {
93
+ const targetStat = await stat(targetPath);
94
+ if (!targetStat.isFile()) {
95
+ return false;
96
+ }
97
+ await access(targetPath, constants.R_OK);
98
+ return true;
99
+ }
100
+ catch {
101
+ return false;
102
+ }
103
+ }
@@ -0,0 +1,3 @@
1
+ export { loadSkills } from "./loadSkills.js";
2
+ export type { LoadedSkill, LoadSkillsConfig, IgnoredDuplicateSkill, LoadSkillsPathError, LoadSkillsPathReport, LoadSkillsReport, LoadSkillsResult, SkillScript, SkillScriptType, SkillState, SkillWarning, SkillWarningCode, } from "./types.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,WAAW,EACX,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,UAAU,EACV,YAAY,EACZ,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { loadSkills } from "./loadSkills.js";
@@ -0,0 +1,3 @@
1
+ import type { LoadSkillsConfig, LoadSkillsResult } from "./types.js";
2
+ export declare function loadSkills(config?: LoadSkillsConfig): Promise<LoadSkillsResult>;
3
+ //# sourceMappingURL=loadSkills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadSkills.d.ts","sourceRoot":"","sources":["../src/loadSkills.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,gBAAgB,EAChB,gBAAgB,EAIjB,MAAM,YAAY,CAAC;AAOpB,wBAAsB,UAAU,CAC9B,MAAM,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,gBAAgB,CAAC,CAoG3B"}
@@ -0,0 +1,217 @@
1
+ import { constants } from "node:fs";
2
+ import { open, readdir, readFile, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { discoverSkillFiles } from "./discovery.js";
5
+ import { parseSkillDocument } from "./parseSkill.js";
6
+ import { getErrorMessage, pathExists } from "./utils.js";
7
+ import { applyValidationRules, validateLargeReferences, } from "./validateSkill.js";
8
+ export async function loadSkills(config) {
9
+ const resolvedConfig = config ?? {};
10
+ const discovery = await discoverSkillFiles(resolvedConfig);
11
+ const results = [];
12
+ const reportPaths = discovery.report.map((entry) => ({ ...entry }));
13
+ const ignoredDuplicates = {};
14
+ const includedByName = new Map();
15
+ for (const discovered of discovery.discoveredSkillFiles) {
16
+ const skillFilePath = discovered.skillFilePath;
17
+ const skillPath = path.dirname(skillFilePath);
18
+ const skillWarnings = [];
19
+ const rawSkillText = await readFile(skillFilePath, "utf8");
20
+ const parsed = parseSkillDocument(rawSkillText);
21
+ skillWarnings.push(...parsed.warnings);
22
+ const resourceScan = await collectResources(skillPath);
23
+ skillWarnings.push(...resourceScan.warnings);
24
+ const largeReferenceWarnings = validateLargeReferences(resourceScan.referenceContents);
25
+ skillWarnings.push(...largeReferenceWarnings);
26
+ const skill = applyValidationRules({
27
+ meta: parsed.meta,
28
+ content: parsed.content,
29
+ references: resourceScan.references,
30
+ scripts: resourceScan.scripts,
31
+ state: "valid",
32
+ warnings: skillWarnings,
33
+ skillPath,
34
+ skillFilePath,
35
+ });
36
+ const reportItem = reportPaths[discovered.pathReportIndex];
37
+ if (!reportItem) {
38
+ continue;
39
+ }
40
+ const resolvedName = typeof skill.meta.name === "string" && skill.meta.name.trim() !== ""
41
+ ? skill.meta.name.trim()
42
+ : path.basename(skill.skillPath);
43
+ if (typeof skill.meta.name === "string" && skill.meta.name.trim() !== "") {
44
+ const normalizedName = skill.meta.name.trim().toLowerCase();
45
+ const existing = includedByName.get(normalizedName);
46
+ if (existing) {
47
+ const ignoredEntry = {
48
+ skillName: resolvedName,
49
+ normalizedSkillName: normalizedName,
50
+ ignoredSkillPath: skill.skillPath,
51
+ ignoredSkillFilePath: skill.skillFilePath,
52
+ ignoredFromInputPath: discovered.inputPath,
53
+ keptSkillPath: existing.skillPath,
54
+ keptSkillFilePath: existing.skillFilePath,
55
+ keptFromInputPath: existing.inputPath,
56
+ };
57
+ const key = existing.skillName;
58
+ ignoredDuplicates[key] = [
59
+ ...(ignoredDuplicates[key] ?? []),
60
+ ignoredEntry,
61
+ ];
62
+ continue;
63
+ }
64
+ includedByName.set(normalizedName, {
65
+ skillName: resolvedName,
66
+ skillPath: skill.skillPath,
67
+ skillFilePath: skill.skillFilePath,
68
+ inputPath: discovered.inputPath,
69
+ });
70
+ }
71
+ results.push(skill);
72
+ reportItem.skillNames.push(resolvedName);
73
+ reportItem.count += 1;
74
+ }
75
+ for (const reportItem of reportPaths) {
76
+ reportItem.skillNames.sort((a, b) => a.localeCompare(b));
77
+ }
78
+ return {
79
+ skills: results,
80
+ report: {
81
+ paths: reportPaths,
82
+ ignoredDuplicates,
83
+ },
84
+ };
85
+ }
86
+ async function collectResources(skillPath) {
87
+ const referencesRoot = path.join(skillPath, "references");
88
+ const scriptsRoot = path.join(skillPath, "scripts");
89
+ const warnings = [];
90
+ const referenceFiles = await listFilesRecursively(referencesRoot, warnings);
91
+ const scriptFiles = await listFilesRecursively(scriptsRoot, warnings);
92
+ const referenceContents = [];
93
+ for (const filePath of referenceFiles) {
94
+ try {
95
+ const content = await readFile(filePath, "utf8");
96
+ referenceContents.push({
97
+ path: filePath,
98
+ content,
99
+ lineCount: content.length === 0 ? 0 : content.split(/\r?\n/).length,
100
+ });
101
+ }
102
+ catch (error) {
103
+ warnings.push({
104
+ code: "resource_read_error",
105
+ message: `Unable to read reference file ${filePath}: ${getErrorMessage(error)}`,
106
+ });
107
+ }
108
+ }
109
+ const scripts = [];
110
+ for (const filePath of scriptFiles) {
111
+ scripts.push({
112
+ path: filePath,
113
+ type: await inferScriptType(filePath),
114
+ });
115
+ }
116
+ return {
117
+ references: referenceFiles.sort((a, b) => a.localeCompare(b)),
118
+ referenceContents,
119
+ scripts: scripts.sort((a, b) => a.path.localeCompare(b.path)),
120
+ warnings,
121
+ };
122
+ }
123
+ async function listFilesRecursively(maybeRoot, warnings) {
124
+ const exists = await pathExists(maybeRoot);
125
+ if (!exists) {
126
+ return [];
127
+ }
128
+ const rootStats = await stat(maybeRoot);
129
+ if (!rootStats.isDirectory()) {
130
+ warnings.push({
131
+ code: "resource_read_error",
132
+ message: `Expected directory but found non-directory resource root: ${maybeRoot}`,
133
+ });
134
+ return [];
135
+ }
136
+ const files = [];
137
+ async function walk(current) {
138
+ let entries;
139
+ try {
140
+ entries = await readdir(current, { withFileTypes: true });
141
+ }
142
+ catch (error) {
143
+ warnings.push({
144
+ code: "resource_read_error",
145
+ message: `Unable to read resource directory ${current}: ${getErrorMessage(error)}`,
146
+ });
147
+ return;
148
+ }
149
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
150
+ const fullPath = path.join(current, entry.name);
151
+ if (entry.isDirectory()) {
152
+ await walk(fullPath);
153
+ }
154
+ else if (entry.isFile()) {
155
+ files.push(fullPath);
156
+ }
157
+ }
158
+ }
159
+ await walk(maybeRoot);
160
+ return files;
161
+ }
162
+ async function inferScriptType(filePath) {
163
+ const ext = path.extname(filePath).toLowerCase();
164
+ if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
165
+ return "javascript";
166
+ }
167
+ if (ext === ".ts" || ext === ".mts" || ext === ".cts") {
168
+ return "typescript";
169
+ }
170
+ if (ext === ".py") {
171
+ return "python";
172
+ }
173
+ if (ext === ".sh" || ext === ".bash" || ext === ".zsh") {
174
+ return "shell";
175
+ }
176
+ if (ext === ".rb") {
177
+ return "ruby";
178
+ }
179
+ try {
180
+ const firstLine = await readFirstLine(filePath);
181
+ if (firstLine.startsWith("#!")) {
182
+ if (firstLine.includes("python")) {
183
+ return "python";
184
+ }
185
+ if (firstLine.includes("bash") ||
186
+ firstLine.includes("sh") ||
187
+ firstLine.includes("zsh")) {
188
+ return "shell";
189
+ }
190
+ if (firstLine.includes("ruby")) {
191
+ return "ruby";
192
+ }
193
+ if (firstLine.includes("node") ||
194
+ firstLine.includes("deno") ||
195
+ firstLine.includes("bun")) {
196
+ return "javascript";
197
+ }
198
+ }
199
+ }
200
+ catch {
201
+ // If we cannot inspect shebang, keep fallback type.
202
+ }
203
+ return "other";
204
+ }
205
+ async function readFirstLine(filePath) {
206
+ const handle = await open(filePath, constants.O_RDONLY);
207
+ try {
208
+ const maxBytes = 1024;
209
+ const buffer = Buffer.alloc(maxBytes);
210
+ const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
211
+ const snippet = buffer.subarray(0, bytesRead).toString("utf8");
212
+ return snippet.split(/\r?\n/, 1)[0]?.toLowerCase() ?? "";
213
+ }
214
+ finally {
215
+ await handle.close();
216
+ }
217
+ }
@@ -0,0 +1,3 @@
1
+ import type { ParseSkillResult } from "./types.js";
2
+ export declare function parseSkillDocument(rawText: string): ParseSkillResult;
3
+ //# sourceMappingURL=parseSkill.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseSkill.d.ts","sourceRoot":"","sources":["../src/parseSkill.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAgB,MAAM,YAAY,CAAC;AAGjE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAwFpE"}
@@ -0,0 +1,77 @@
1
+ import { parse as parseYaml } from "yaml";
2
+ import { getErrorMessage } from "./utils.js";
3
+ export function parseSkillDocument(rawText) {
4
+ const warnings = [];
5
+ const text = rawText.replace(/^\uFEFF/, "");
6
+ if (!text.startsWith("---\n") && !text.startsWith("---\r\n")) {
7
+ warnings.push({
8
+ code: "missing_frontmatter",
9
+ message: "SKILL.md is missing YAML frontmatter delimiters.",
10
+ });
11
+ return {
12
+ meta: {},
13
+ content: text,
14
+ warnings,
15
+ };
16
+ }
17
+ const lines = text.split(/\r?\n/);
18
+ let closingIndex = -1;
19
+ for (let i = 1; i < lines.length; i += 1) {
20
+ if (lines[i] === "---") {
21
+ closingIndex = i;
22
+ break;
23
+ }
24
+ }
25
+ if (closingIndex === -1) {
26
+ warnings.push({
27
+ code: "missing_frontmatter",
28
+ message: "Frontmatter opening delimiter exists but closing delimiter is missing.",
29
+ });
30
+ return {
31
+ meta: {},
32
+ content: text,
33
+ warnings,
34
+ };
35
+ }
36
+ const frontmatterRaw = lines.slice(1, closingIndex).join("\n");
37
+ const body = lines.slice(closingIndex + 1).join("\n");
38
+ if (frontmatterRaw.trim() === "") {
39
+ return {
40
+ meta: {},
41
+ content: body,
42
+ warnings,
43
+ };
44
+ }
45
+ try {
46
+ const parsed = parseYaml(frontmatterRaw);
47
+ if (parsed === null ||
48
+ typeof parsed !== "object" ||
49
+ Array.isArray(parsed)) {
50
+ warnings.push({
51
+ code: "invalid_yaml_frontmatter",
52
+ message: "Frontmatter must parse to a YAML object.",
53
+ });
54
+ return {
55
+ meta: {},
56
+ content: body,
57
+ warnings,
58
+ };
59
+ }
60
+ return {
61
+ meta: parsed,
62
+ content: body,
63
+ warnings,
64
+ };
65
+ }
66
+ catch (error) {
67
+ warnings.push({
68
+ code: "invalid_yaml_frontmatter",
69
+ message: `Unable to parse YAML frontmatter: ${getErrorMessage(error)}`,
70
+ });
71
+ return {
72
+ meta: {},
73
+ content: body,
74
+ warnings,
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,58 @@
1
+ export type SkillState = "valid" | "invalid";
2
+ export type SkillScriptType = "javascript" | "typescript" | "python" | "shell" | "ruby" | "other";
3
+ export type SkillWarningCode = "missing_frontmatter" | "invalid_yaml_frontmatter" | "missing_required_meta_name" | "missing_required_meta_description" | "invalid_meta_name" | "invalid_meta_description" | "skill_md_content_size_limit_exceeded" | "reference_large_without_toc" | "resource_read_error";
4
+ export interface SkillWarning {
5
+ code: SkillWarningCode;
6
+ message: string;
7
+ }
8
+ export interface SkillScript {
9
+ path: string;
10
+ type: SkillScriptType;
11
+ }
12
+ export interface LoadedSkill {
13
+ meta: Record<string, unknown>;
14
+ content: string;
15
+ references: string[];
16
+ scripts: SkillScript[];
17
+ state: SkillState;
18
+ warnings: SkillWarning[];
19
+ skillPath: string;
20
+ skillFilePath: string;
21
+ }
22
+ export interface LoadSkillsConfig {
23
+ paths?: string[];
24
+ recursive?: boolean;
25
+ cwd?: string;
26
+ }
27
+ export type LoadSkillsPathError = "path_not_found" | "path_not_directory";
28
+ export interface LoadSkillsPathReport {
29
+ inputPath: string;
30
+ resolvedPath: string;
31
+ count: number;
32
+ skillNames: string[];
33
+ error?: LoadSkillsPathError;
34
+ }
35
+ export interface IgnoredDuplicateSkill {
36
+ skillName: string;
37
+ normalizedSkillName: string;
38
+ ignoredSkillPath: string;
39
+ ignoredSkillFilePath: string;
40
+ ignoredFromInputPath: string;
41
+ keptSkillPath: string;
42
+ keptSkillFilePath: string;
43
+ keptFromInputPath: string;
44
+ }
45
+ export interface LoadSkillsReport {
46
+ paths: LoadSkillsPathReport[];
47
+ ignoredDuplicates: Record<string, IgnoredDuplicateSkill[]>;
48
+ }
49
+ export interface LoadSkillsResult {
50
+ skills: LoadedSkill[];
51
+ report: LoadSkillsReport;
52
+ }
53
+ export interface ParseSkillResult {
54
+ meta: Record<string, unknown>;
55
+ content: string;
56
+ warnings: SkillWarning[];
57
+ }
58
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAE7C,MAAM,MAAM,eAAe,GACvB,YAAY,GACZ,YAAY,GACZ,QAAQ,GACR,OAAO,GACP,MAAM,GACN,OAAO,CAAC;AAEZ,MAAM,MAAM,gBAAgB,GACxB,qBAAqB,GACrB,0BAA0B,GAC1B,4BAA4B,GAC5B,mCAAmC,GACnC,mBAAmB,GACnB,0BAA0B,GAC1B,sCAAsC,GACtC,6BAA6B,GAC7B,qBAAqB,CAAC;AAE1B,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,eAAe,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,mBAAmB,GAAG,gBAAgB,GAAG,oBAAoB,CAAC;AAE1E,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,EAAE,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare function pathExists(targetPath: string): Promise<boolean>;
2
+ export declare function getErrorMessage(error: unknown): string;
3
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAGA,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOrE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMtD"}
package/dist/utils.js ADDED
@@ -0,0 +1,17 @@
1
+ import { constants } from "node:fs";
2
+ import { access } from "node:fs/promises";
3
+ export async function pathExists(targetPath) {
4
+ try {
5
+ await access(targetPath, constants.F_OK);
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ export function getErrorMessage(error) {
13
+ if (error instanceof Error) {
14
+ return error.message;
15
+ }
16
+ return String(error);
17
+ }
@@ -0,0 +1,8 @@
1
+ import type { LoadedSkill, SkillWarning } from "./types.js";
2
+ export declare function applyValidationRules(skill: LoadedSkill): LoadedSkill;
3
+ export declare function validateLargeReferences(references: Array<{
4
+ path: string;
5
+ content: string;
6
+ lineCount: number;
7
+ }>): SkillWarning[];
8
+ //# sourceMappingURL=validateSkill.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateSkill.d.ts","sourceRoot":"","sources":["../src/validateSkill.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAc5D,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,WAAW,CA8CpE;AAED,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,GACtE,YAAY,EAAE,CAqBhB"}
@@ -0,0 +1,83 @@
1
+ const INVALID_WARNING_CODES = new Set([
2
+ "missing_frontmatter",
3
+ "invalid_yaml_frontmatter",
4
+ "missing_required_meta_name",
5
+ "missing_required_meta_description",
6
+ "invalid_meta_name",
7
+ "invalid_meta_description",
8
+ "skill_md_content_size_limit_exceeded",
9
+ "reference_large_without_toc",
10
+ "resource_read_error",
11
+ ]);
12
+ export function applyValidationRules(skill) {
13
+ const warnings = [...skill.warnings];
14
+ const metaName = skill.meta.name;
15
+ const metaDescription = skill.meta.description;
16
+ if (typeof metaName !== "string" || metaName.trim() === "") {
17
+ warnings.push({
18
+ code: typeof metaName === "undefined"
19
+ ? "missing_required_meta_name"
20
+ : "invalid_meta_name",
21
+ message: "Frontmatter field `name` is required and must be a non-empty string.",
22
+ });
23
+ }
24
+ if (typeof metaDescription !== "string" || metaDescription.trim() === "") {
25
+ warnings.push({
26
+ code: typeof metaDescription === "undefined"
27
+ ? "missing_required_meta_description"
28
+ : "invalid_meta_description",
29
+ message: "Frontmatter field `description` is required and must be a non-empty string.",
30
+ });
31
+ }
32
+ const contentLineCount = getLineCount(skill.content);
33
+ if (contentLineCount > 500) {
34
+ warnings.push({
35
+ code: "skill_md_content_size_limit_exceeded",
36
+ message: `SKILL.md body exceeds the recommended 500-line limit (${contentLineCount} lines).`,
37
+ });
38
+ }
39
+ const state = warnings.some((warning) => INVALID_WARNING_CODES.has(warning.code))
40
+ ? "invalid"
41
+ : "valid";
42
+ return {
43
+ ...skill,
44
+ warnings: dedupeWarnings(warnings),
45
+ state,
46
+ };
47
+ }
48
+ export function validateLargeReferences(references) {
49
+ const warnings = [];
50
+ for (const reference of references) {
51
+ if (reference.lineCount <= 300) {
52
+ continue;
53
+ }
54
+ const normalized = reference.content.toLowerCase();
55
+ const hasTocMarker = normalized.includes("table of contents") || normalized.includes("[toc]");
56
+ if (!hasTocMarker) {
57
+ warnings.push({
58
+ code: "reference_large_without_toc",
59
+ message: `Large reference file is missing a table of contents hint: ${reference.path}`,
60
+ });
61
+ }
62
+ }
63
+ return warnings;
64
+ }
65
+ function dedupeWarnings(warnings) {
66
+ const keys = new Set();
67
+ const deduped = [];
68
+ for (const warning of warnings) {
69
+ const key = `${warning.code}:${warning.message}`;
70
+ if (keys.has(key)) {
71
+ continue;
72
+ }
73
+ keys.add(key);
74
+ deduped.push(warning);
75
+ }
76
+ return deduped;
77
+ }
78
+ function getLineCount(content) {
79
+ if (content.length === 0) {
80
+ return 0;
81
+ }
82
+ return content.split(/\r?\n/).length;
83
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "load-skills",
3
+ "version": "0.1.0",
4
+ "description": "Load and validate agent skills from configurable paths.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "fmt": "prettier -w .",
20
+ "fmt:check": "prettier -c .",
21
+ "prepare": "husky",
22
+ "precommit:check": "bun run typecheck && bun run test && bun run fmt:check",
23
+ "typecheck": "tsc -p tsconfig.json --noEmit",
24
+ "test": "vitest run"
25
+ },
26
+ "keywords": [
27
+ "skills",
28
+ "agent",
29
+ "loader",
30
+ "frontmatter"
31
+ ],
32
+ "author": "",
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "yaml": "^2.8.2"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^25.5.0",
39
+ "husky": "^9.1.7",
40
+ "prettier": "^3.6.2",
41
+ "typescript": "^5.9.3",
42
+ "vitest": "^4.1.0"
43
+ }
44
+ }