@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/LICENSE
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to the Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding any notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
Copyright 2026 Piyush Vyas (@pyyush)
|
|
179
|
+
|
|
180
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
|
+
you may not use this file except in compliance with the License.
|
|
182
|
+
You may obtain a copy of the License at
|
|
183
|
+
|
|
184
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
185
|
+
|
|
186
|
+
Unless required by applicable law or agreed to in writing, software
|
|
187
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
188
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
189
|
+
See the License for the specific language governing permissions and
|
|
190
|
+
limitations under the License.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var promises = require('fs/promises');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var matter = require('gray-matter');
|
|
7
|
+
var jsTiktoken = require('js-tiktoken');
|
|
8
|
+
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var matter__default = /*#__PURE__*/_interopDefault(matter);
|
|
12
|
+
|
|
13
|
+
// src/parser.ts
|
|
14
|
+
var encoder = null;
|
|
15
|
+
function getEncoder() {
|
|
16
|
+
if (!encoder) {
|
|
17
|
+
encoder = jsTiktoken.encodingForModel("gpt-4o");
|
|
18
|
+
}
|
|
19
|
+
return encoder;
|
|
20
|
+
}
|
|
21
|
+
function countTokens(text) {
|
|
22
|
+
if (text.length === 0) {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
const enc = getEncoder();
|
|
26
|
+
return enc.encode(text).length;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/parser.ts
|
|
30
|
+
var FILE_REFERENCE_PATTERN = /(?:^|\s|`)((?:scripts|references|assets)\/[\w./-]+(?:\.\w+)?)/gm;
|
|
31
|
+
var MARKDOWN_LINK_PATTERN = /\[([^\]]*)\]\((?!https?:\/\/)([^)]+)\)/g;
|
|
32
|
+
var NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]?$/;
|
|
33
|
+
var CONSECUTIVE_HYPHENS = /--/;
|
|
34
|
+
async function parseSkill(filePath) {
|
|
35
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
|
|
36
|
+
const dirPath = path.dirname(absolutePath);
|
|
37
|
+
let rawContent;
|
|
38
|
+
try {
|
|
39
|
+
rawContent = await promises.readFile(absolutePath, "utf-8");
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
skill: null,
|
|
44
|
+
diagnostics: [
|
|
45
|
+
{
|
|
46
|
+
ruleId: "file-readable",
|
|
47
|
+
severity: "error",
|
|
48
|
+
message: `Cannot read file: ${err instanceof Error ? err.message : String(err)}`,
|
|
49
|
+
file: absolutePath
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (rawContent.trim().length === 0) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
skill: null,
|
|
58
|
+
diagnostics: [
|
|
59
|
+
{
|
|
60
|
+
ruleId: "file-not-empty",
|
|
61
|
+
severity: "error",
|
|
62
|
+
message: "SKILL.md file is empty",
|
|
63
|
+
file: absolutePath
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return parseSkillContent(rawContent, absolutePath, dirPath);
|
|
69
|
+
}
|
|
70
|
+
function parseSkillContent(rawContent, filePath, dirPath) {
|
|
71
|
+
const diagnostics = [];
|
|
72
|
+
let hasErrors = false;
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = matter__default.default(rawContent);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
skill: null,
|
|
80
|
+
diagnostics: [
|
|
81
|
+
{
|
|
82
|
+
ruleId: "frontmatter-valid-yaml",
|
|
83
|
+
severity: "error",
|
|
84
|
+
message: `Invalid YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
|
|
85
|
+
file: filePath,
|
|
86
|
+
line: 1
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const hasFrontmatter = rawContent.trimStart().startsWith("---") && Object.keys(parsed.data).length > 0;
|
|
92
|
+
if (!hasFrontmatter) {
|
|
93
|
+
diagnostics.push({
|
|
94
|
+
ruleId: "frontmatter-required",
|
|
95
|
+
severity: "error",
|
|
96
|
+
message: "SKILL.md must have YAML frontmatter between --- delimiters",
|
|
97
|
+
file: filePath,
|
|
98
|
+
line: 1,
|
|
99
|
+
fix: "Add frontmatter at the top of the file:\n---\nname: my-skill\ndescription: A short description\n---"
|
|
100
|
+
});
|
|
101
|
+
hasErrors = true;
|
|
102
|
+
}
|
|
103
|
+
const data = parsed.data;
|
|
104
|
+
if (data.name != null && typeof data.name !== "string") {
|
|
105
|
+
diagnostics.push({
|
|
106
|
+
ruleId: "name-type",
|
|
107
|
+
severity: "error",
|
|
108
|
+
message: 'Frontmatter "name" field must be a string',
|
|
109
|
+
file: filePath,
|
|
110
|
+
line: 1,
|
|
111
|
+
fix: "Ensure the name is a string: name: my-skill-name"
|
|
112
|
+
});
|
|
113
|
+
hasErrors = true;
|
|
114
|
+
} else if (data.name == null || typeof data.name === "string" && data.name.trim().length === 0) {
|
|
115
|
+
diagnostics.push({
|
|
116
|
+
ruleId: "name-required",
|
|
117
|
+
severity: "error",
|
|
118
|
+
message: 'Frontmatter must contain a "name" field (required by spec)',
|
|
119
|
+
file: filePath,
|
|
120
|
+
line: 1,
|
|
121
|
+
fix: "Add a name field: name: my-skill-name"
|
|
122
|
+
});
|
|
123
|
+
hasErrors = true;
|
|
124
|
+
} else if (typeof data.name === "string") {
|
|
125
|
+
if (!NAME_PATTERN.test(data.name)) {
|
|
126
|
+
diagnostics.push({
|
|
127
|
+
ruleId: "name-format",
|
|
128
|
+
severity: "error",
|
|
129
|
+
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`,
|
|
130
|
+
file: filePath,
|
|
131
|
+
line: 1,
|
|
132
|
+
fix: `Use a name like: ${String(data.name).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}`
|
|
133
|
+
});
|
|
134
|
+
hasErrors = true;
|
|
135
|
+
} else if (CONSECUTIVE_HYPHENS.test(data.name)) {
|
|
136
|
+
diagnostics.push({
|
|
137
|
+
ruleId: "name-format",
|
|
138
|
+
severity: "error",
|
|
139
|
+
message: `Skill name "${data.name}" contains consecutive hyphens (--), which is not allowed`,
|
|
140
|
+
file: filePath,
|
|
141
|
+
line: 1,
|
|
142
|
+
fix: `Replace consecutive hyphens with single hyphens: ${data.name.replace(/--+/g, "-")}`
|
|
143
|
+
});
|
|
144
|
+
hasErrors = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (data.description != null && typeof data.description !== "string") {
|
|
148
|
+
diagnostics.push({
|
|
149
|
+
ruleId: "description-type",
|
|
150
|
+
severity: "error",
|
|
151
|
+
message: 'Frontmatter "description" field must be a string',
|
|
152
|
+
file: filePath,
|
|
153
|
+
line: 1,
|
|
154
|
+
fix: 'Ensure the description is a string: description: "A clear description"'
|
|
155
|
+
});
|
|
156
|
+
hasErrors = true;
|
|
157
|
+
} else if (data.description == null || typeof data.description === "string" && data.description.trim().length === 0) {
|
|
158
|
+
diagnostics.push({
|
|
159
|
+
ruleId: "description-required",
|
|
160
|
+
severity: "error",
|
|
161
|
+
message: 'Frontmatter must contain a "description" field (required by spec)',
|
|
162
|
+
file: filePath,
|
|
163
|
+
line: 1,
|
|
164
|
+
fix: 'Add a description: description: "What this skill does and when to use it"'
|
|
165
|
+
});
|
|
166
|
+
hasErrors = true;
|
|
167
|
+
} else if (typeof data.description === "string") {
|
|
168
|
+
const descLen = data.description.length;
|
|
169
|
+
if (descLen < 10) {
|
|
170
|
+
diagnostics.push({
|
|
171
|
+
ruleId: "description-length",
|
|
172
|
+
severity: "warning",
|
|
173
|
+
message: `Description is too short (${descLen} chars). Should be at least 50 characters for effective agent routing`,
|
|
174
|
+
file: filePath,
|
|
175
|
+
line: 1,
|
|
176
|
+
fix: "Expand the description to explain what the skill does, when to use it, and what triggers it"
|
|
177
|
+
});
|
|
178
|
+
} else if (descLen > 1024) {
|
|
179
|
+
diagnostics.push({
|
|
180
|
+
ruleId: "description-length",
|
|
181
|
+
severity: "error",
|
|
182
|
+
message: `Description exceeds max length (${descLen} chars). Must be at most 1,024 characters per spec`,
|
|
183
|
+
file: filePath,
|
|
184
|
+
line: 1,
|
|
185
|
+
fix: "Shorten the description to the essential information. Move details to the instructions body"
|
|
186
|
+
});
|
|
187
|
+
hasErrors = true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const body = parsed.content.trim();
|
|
191
|
+
if (body.length === 0) {
|
|
192
|
+
diagnostics.push({
|
|
193
|
+
ruleId: "body-required",
|
|
194
|
+
severity: "error",
|
|
195
|
+
message: "SKILL.md must have markdown content after the frontmatter",
|
|
196
|
+
file: filePath,
|
|
197
|
+
fix: "Add instructions below the frontmatter that teach an agent how to use this skill"
|
|
198
|
+
});
|
|
199
|
+
hasErrors = true;
|
|
200
|
+
}
|
|
201
|
+
const sections = parseSections(parsed.content);
|
|
202
|
+
const fileReferences = extractFileReferences(parsed.content, filePath, dirPath);
|
|
203
|
+
for (const ref of fileReferences) {
|
|
204
|
+
if (!ref.exists) {
|
|
205
|
+
diagnostics.push({
|
|
206
|
+
ruleId: "file-reference-exists",
|
|
207
|
+
severity: "error",
|
|
208
|
+
message: `Referenced file not found: ${ref.path}`,
|
|
209
|
+
file: filePath,
|
|
210
|
+
line: ref.line,
|
|
211
|
+
fix: `Create the file at ${ref.path} or remove the reference`
|
|
212
|
+
});
|
|
213
|
+
hasErrors = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const tokenCount = countTokens(rawContent);
|
|
217
|
+
if (tokenCount > 5e3) {
|
|
218
|
+
diagnostics.push({
|
|
219
|
+
ruleId: "token-budget",
|
|
220
|
+
severity: "warning",
|
|
221
|
+
message: `Token count (${tokenCount}) exceeds the recommended 5,000 token budget`,
|
|
222
|
+
file: filePath,
|
|
223
|
+
fix: "Move detailed content to references/ directory to keep the main SKILL.md lean"
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const knownFields = ["name", "description", "version"];
|
|
227
|
+
const dangerousKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
228
|
+
const metadata = {
|
|
229
|
+
...typeof data.name === "string" ? { name: data.name } : {},
|
|
230
|
+
...typeof data.description === "string" ? { description: data.description } : {},
|
|
231
|
+
...data.version != null ? { version: String(data.version) } : {},
|
|
232
|
+
...Object.fromEntries(
|
|
233
|
+
Object.entries(data).filter(([k]) => !knownFields.includes(k) && !dangerousKeys.has(k))
|
|
234
|
+
)
|
|
235
|
+
};
|
|
236
|
+
if (hasErrors) {
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
skill: null,
|
|
240
|
+
diagnostics
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const lineCount = body.split("\n").length;
|
|
244
|
+
const skill = {
|
|
245
|
+
metadata,
|
|
246
|
+
body,
|
|
247
|
+
sections,
|
|
248
|
+
fileReferences,
|
|
249
|
+
filePath,
|
|
250
|
+
dirPath,
|
|
251
|
+
tokenCount,
|
|
252
|
+
lineCount,
|
|
253
|
+
rawContent
|
|
254
|
+
};
|
|
255
|
+
return { ok: true, skill, diagnostics };
|
|
256
|
+
}
|
|
257
|
+
function parseSections(content, _filePath) {
|
|
258
|
+
const lines = content.split("\n");
|
|
259
|
+
const sections = [];
|
|
260
|
+
let currentSection = null;
|
|
261
|
+
const frontmatterOffset = 0;
|
|
262
|
+
for (let i = 0; i < lines.length; i++) {
|
|
263
|
+
const line = lines[i];
|
|
264
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
265
|
+
if (headingMatch) {
|
|
266
|
+
if (currentSection) {
|
|
267
|
+
sections.push({
|
|
268
|
+
heading: currentSection.heading,
|
|
269
|
+
depth: currentSection.depth,
|
|
270
|
+
content: currentSection.lines.join("\n").trim(),
|
|
271
|
+
line: currentSection.line
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
currentSection = {
|
|
275
|
+
heading: headingMatch[2],
|
|
276
|
+
depth: headingMatch[1].length,
|
|
277
|
+
line: frontmatterOffset + i + 1,
|
|
278
|
+
lines: []
|
|
279
|
+
};
|
|
280
|
+
} else if (currentSection) {
|
|
281
|
+
currentSection.lines.push(line);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (currentSection) {
|
|
285
|
+
sections.push({
|
|
286
|
+
heading: currentSection.heading,
|
|
287
|
+
depth: currentSection.depth,
|
|
288
|
+
content: currentSection.lines.join("\n").trim(),
|
|
289
|
+
line: currentSection.line
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return sections;
|
|
293
|
+
}
|
|
294
|
+
function extractFileReferences(content, _filePath, dirPath) {
|
|
295
|
+
const references = [];
|
|
296
|
+
const seen = /* @__PURE__ */ new Set();
|
|
297
|
+
const lines = content.split("\n");
|
|
298
|
+
for (let i = 0; i < lines.length; i++) {
|
|
299
|
+
const line = lines[i];
|
|
300
|
+
for (const m of line.matchAll(FILE_REFERENCE_PATTERN)) {
|
|
301
|
+
const refPath = m[1];
|
|
302
|
+
if (seen.has(refPath)) continue;
|
|
303
|
+
seen.add(refPath);
|
|
304
|
+
const absoluteRefPath = path.resolve(dirPath, refPath);
|
|
305
|
+
if (!absoluteRefPath.startsWith(`${path.resolve(dirPath)}/`)) continue;
|
|
306
|
+
references.push({
|
|
307
|
+
path: refPath,
|
|
308
|
+
line: i + 1,
|
|
309
|
+
exists: fs.existsSync(absoluteRefPath)
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
for (const m of line.matchAll(MARKDOWN_LINK_PATTERN)) {
|
|
313
|
+
const refPath = m[2];
|
|
314
|
+
if (refPath.startsWith("#") || refPath.startsWith("data:") || seen.has(refPath)) continue;
|
|
315
|
+
seen.add(refPath);
|
|
316
|
+
const absoluteRefPath = path.resolve(dirPath, refPath);
|
|
317
|
+
if (!absoluteRefPath.startsWith(`${path.resolve(dirPath)}/`)) continue;
|
|
318
|
+
references.push({
|
|
319
|
+
path: refPath,
|
|
320
|
+
line: i + 1,
|
|
321
|
+
exists: fs.existsSync(absoluteRefPath)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return references;
|
|
326
|
+
}
|
|
327
|
+
async function resolveSkillFiles(searchPath) {
|
|
328
|
+
const absolutePath = path.resolve(searchPath);
|
|
329
|
+
const locations = [];
|
|
330
|
+
const pathStat = await promises.stat(absolutePath).catch(() => null);
|
|
331
|
+
if (!pathStat) {
|
|
332
|
+
return locations;
|
|
333
|
+
}
|
|
334
|
+
if (pathStat.isFile() && path.basename(absolutePath) === "SKILL.md") {
|
|
335
|
+
const directory = path.resolve(absolutePath, "..");
|
|
336
|
+
locations.push({
|
|
337
|
+
skillFile: absolutePath,
|
|
338
|
+
directory,
|
|
339
|
+
dirName: path.basename(directory)
|
|
340
|
+
});
|
|
341
|
+
return locations;
|
|
342
|
+
}
|
|
343
|
+
if (!pathStat.isDirectory()) {
|
|
344
|
+
return locations;
|
|
345
|
+
}
|
|
346
|
+
const directSkill = path.join(absolutePath, "SKILL.md");
|
|
347
|
+
const directStat = await promises.stat(directSkill).catch(() => null);
|
|
348
|
+
if (directStat?.isFile()) {
|
|
349
|
+
locations.push({
|
|
350
|
+
skillFile: directSkill,
|
|
351
|
+
directory: absolutePath,
|
|
352
|
+
dirName: path.basename(absolutePath)
|
|
353
|
+
});
|
|
354
|
+
return locations;
|
|
355
|
+
}
|
|
356
|
+
const entries = await promises.readdir(absolutePath, { withFileTypes: true });
|
|
357
|
+
const subdirChecks = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map(async (entry) => {
|
|
358
|
+
const subdir = path.join(absolutePath, entry.name);
|
|
359
|
+
const skillFile = path.join(subdir, "SKILL.md");
|
|
360
|
+
const skillStat = await promises.stat(skillFile).catch(() => null);
|
|
361
|
+
if (skillStat?.isFile()) {
|
|
362
|
+
locations.push({
|
|
363
|
+
skillFile,
|
|
364
|
+
directory: subdir,
|
|
365
|
+
dirName: entry.name
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
await Promise.all(subdirChecks);
|
|
370
|
+
locations.sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
371
|
+
return locations;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
exports.countTokens = countTokens;
|
|
375
|
+
exports.parseSkill = parseSkill;
|
|
376
|
+
exports.parseSkillContent = parseSkillContent;
|
|
377
|
+
exports.resolveSkillFiles = resolveSkillFiles;
|
|
378
|
+
//# sourceMappingURL=index.cjs.map
|
|
379
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tokenizer.ts","../src/parser.ts","../src/resolver.ts"],"names":["encodingForModel","isAbsolute","resolve","dirname","readFile","matter","existsSync","stat","basename","join","readdir"],"mappings":"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","file":"index.cjs","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"]}
|