@dinoxx/dinox-cli 1.1.1 → 2.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 +44 -3
- package/dist/cli.js +4 -0
- package/dist/commands/auth/index.js +1 -2
- package/dist/commands/boxes/index.js +37 -13
- package/dist/commands/boxes/operations.d.ts +3 -3
- package/dist/commands/boxes/operations.js +51 -28
- package/dist/commands/info/index.js +30 -5
- package/dist/commands/meta/repo.js +2 -1
- package/dist/commands/notes/actions.js +20 -37
- package/dist/commands/notes/bulk.d.ts +2 -2
- package/dist/commands/notes/bulk.js +3 -18
- package/dist/commands/notes/contentPatch.d.ts +5 -1
- package/dist/commands/notes/contentPatch.js +12 -1
- package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
- package/dist/commands/notes/contentPatchCommand.js +29 -8
- package/dist/commands/notes/descriptors.js +19 -16
- package/dist/commands/notes/export.js +3 -0
- package/dist/commands/notes/repo/read.d.ts +8 -0
- package/dist/commands/notes/repo/read.js +321 -0
- package/dist/commands/notes/repo/resolve.d.ts +8 -0
- package/dist/commands/notes/repo/resolve.js +187 -0
- package/dist/commands/notes/repo/search.d.ts +42 -0
- package/dist/commands/notes/repo/search.js +513 -0
- package/dist/commands/notes/repo/shared.d.ts +190 -0
- package/dist/commands/notes/repo/shared.js +102 -0
- package/dist/commands/notes/repo/write.d.ts +24 -0
- package/dist/commands/notes/repo/write.js +251 -0
- package/dist/commands/notes/repo.d.ts +7 -177
- package/dist/commands/notes/repo.js +7 -1310
- package/dist/commands/notes/searchOutput.d.ts +2 -1
- package/dist/commands/notes/searchOutput.js +2 -1
- package/dist/commands/notes/searchSql.js +9 -3
- package/dist/commands/prompt/index.d.ts +1 -0
- package/dist/commands/prompt/index.js +26 -11
- package/dist/commands/savedViews/index.d.ts +7 -0
- package/dist/commands/savedViews/index.js +458 -0
- package/dist/commands/savedViews/model.d.ts +87 -0
- package/dist/commands/savedViews/model.js +306 -0
- package/dist/commands/savedViews/repo.d.ts +123 -0
- package/dist/commands/savedViews/repo.js +512 -0
- package/dist/commands/savedViews/semantics.d.ts +46 -0
- package/dist/commands/savedViews/semantics.js +738 -0
- package/dist/commands/schema/commandMetadata.js +4 -4
- package/dist/commands/schema/index.d.ts +1 -0
- package/dist/commands/schema/index.js +23 -2
- package/dist/commands/schema/renderSkills.d.ts +6 -0
- package/dist/commands/schema/renderSkills.js +109 -11
- package/dist/commands/schema/spec.js +4 -0
- package/dist/commands/skills/index.d.ts +80 -0
- package/dist/commands/skills/index.js +572 -0
- package/dist/commands/storage/index.js +2 -2
- package/dist/commands/storage/repo/configs.d.ts +30 -0
- package/dist/commands/storage/repo/configs.js +259 -0
- package/dist/commands/storage/repo/operations.d.ts +25 -0
- package/dist/commands/storage/repo/operations.js +337 -0
- package/dist/commands/storage/repo/remote.d.ts +32 -0
- package/dist/commands/storage/repo/remote.js +132 -0
- package/dist/commands/storage/repo/resources.d.ts +12 -0
- package/dist/commands/storage/repo/resources.js +134 -0
- package/dist/commands/storage/repo/shared.d.ts +181 -0
- package/dist/commands/storage/repo/shared.js +36 -0
- package/dist/commands/storage/repo/targets.d.ts +12 -0
- package/dist/commands/storage/repo/targets.js +178 -0
- package/dist/commands/storage/repo.d.ts +3 -166
- package/dist/commands/storage/repo.js +4 -1034
- package/dist/commands/sync.js +2 -1
- package/dist/commands/tags/index.js +30 -11
- package/dist/commands/tags/operations.d.ts +3 -3
- package/dist/commands/tags/operations.js +45 -20
- package/dist/commands/templates/model.d.ts +50 -0
- package/dist/commands/templates/model.js +239 -0
- package/dist/commands/todo/index.js +3 -3
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +46 -9
- package/dist/commands/writeConfirmation.d.ts +13 -0
- package/dist/commands/writeConfirmation.js +54 -0
- package/dist/config/resolve.js +5 -10
- package/dist/config/types.d.ts +3 -6
- package/dist/constants/links.d.ts +18 -1
- package/dist/constants/links.js +26 -1
- package/dist/contract/snapshot.d.ts +30 -0
- package/dist/contract/snapshot.js +114 -0
- package/dist/daemon/client.d.ts +2 -2
- package/dist/daemon/client.js +108 -54
- package/dist/daemon/index.d.ts +5 -3
- package/dist/daemon/index.js +28 -5
- package/dist/daemon/lifecycle.d.ts +7 -2
- package/dist/daemon/lifecycle.js +53 -13
- package/dist/daemon/pid.d.ts +1 -1
- package/dist/daemon/pid.js +6 -3
- package/dist/dinox.js +9 -1
- package/dist/powersync/exclusiveRuntime.d.ts +1 -0
- package/dist/powersync/exclusiveRuntime.js +26 -8
- package/dist/powersync/runtime.d.ts +3 -2
- package/dist/powersync/runtime.js +19 -3
- package/dist/powersync/schema/content.d.ts +1 -78
- package/dist/powersync/schema/content.js +1 -78
- package/dist/powersync/schema/electronCanonical.d.ts +1561 -0
- package/dist/powersync/schema/electronCanonical.js +1154 -0
- package/dist/powersync/schema/index.d.ts +573 -129
- package/dist/powersync/schema/index.js +3 -32
- package/dist/powersync/schema/local.d.ts +1 -56
- package/dist/powersync/schema/local.js +1 -60
- package/dist/powersync/schema/note.d.ts +1 -35
- package/dist/powersync/schema/note.js +1 -35
- package/dist/powersync/schema/notesExtras.d.ts +1 -62
- package/dist/powersync/schema/notesExtras.js +1 -71
- package/dist/powersync/schema/projects.d.ts +1 -104
- package/dist/powersync/schema/projects.js +1 -104
- package/dist/powersync/schema/savedViews.d.ts +1 -0
- package/dist/powersync/schema/savedViews.js +1 -0
- package/dist/powersync/schema/storage.d.ts +1 -24
- package/dist/powersync/schema/storage.js +1 -24
- package/dist/powersync/schema/tags.d.ts +1 -37
- package/dist/powersync/schema/tags.js +1 -37
- package/dist/powersync/syncGate.d.ts +20 -2
- package/dist/powersync/uploader.d.ts +16 -2
- package/dist/powersync/uploader.js +152 -158
- package/dist/skills/check.d.ts +1 -0
- package/dist/skills/check.js +538 -70
- package/dist/skills/commandRefs.d.ts +21 -0
- package/dist/skills/commandRefs.js +132 -0
- package/dist/skills/generate.d.ts +13 -0
- package/dist/skills/generate.js +80 -0
- package/dist/skills/registry.d.ts +13 -0
- package/dist/skills/registry.js +58 -0
- package/dist/skills/retired.d.ts +60 -0
- package/dist/skills/retired.js +58 -0
- package/dist/utils/deadline.d.ts +8 -0
- package/dist/utils/deadline.js +31 -0
- package/dist/utils/dryRun.d.ts +20 -1
- package/dist/utils/dryRun.js +22 -3
- package/dist/utils/errorCodes.d.ts +311 -0
- package/dist/utils/errorCodes.js +92 -0
- package/dist/utils/errors.d.ts +15 -2
- package/dist/utils/errors.js +13 -0
- package/dist/utils/processExit.d.ts +1 -0
- package/dist/utils/processExit.js +33 -0
- package/dist/utils/updateNotice.d.ts +1 -0
- package/dist/utils/updateNotice.js +1 -1
- package/package.json +18 -8
package/dist/skills/check.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import remarkParse from 'remark-parse';
|
|
4
|
+
import { unified } from 'unified';
|
|
5
|
+
import { isMap, isScalar, parseDocument } from 'yaml';
|
|
3
6
|
import { findCommandSchema, listCommandSchemas } from '../commands/schema/spec.js';
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
7
|
+
const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
8
|
+
const SKILL_NAME_PATTERN = /^(?!.*--)[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
9
|
+
const SKILL_DESCRIPTION_CATALOG_MAX_CHARS = 160;
|
|
10
|
+
const SKILL_DESCRIPTION_FIRST_SENTENCE_MAX_CHARS = 60;
|
|
11
|
+
const PORTABLE_FRONTMATTER_FIELDS = new Set([
|
|
12
|
+
'name',
|
|
13
|
+
'description',
|
|
14
|
+
'license',
|
|
15
|
+
'compatibility',
|
|
16
|
+
'metadata',
|
|
17
|
+
'allowed-tools',
|
|
18
|
+
]);
|
|
19
|
+
const SKILL_MAX_LINES = 500;
|
|
20
|
+
const SKILL_EVALS_RELATIVE_PATH = path.join('evals', 'evals.json');
|
|
21
|
+
const MIN_OUTPUT_EVALS = 3;
|
|
22
|
+
const MIN_TRIGGER_EVALS_PER_SPLIT = 4;
|
|
9
23
|
const commandSchemas = listCommandSchemas();
|
|
10
24
|
const commandSchemaPrefixes = commandSchemas
|
|
11
25
|
.map((schema) => ({
|
|
@@ -26,17 +40,58 @@ function parseFrontmatter(content) {
|
|
|
26
40
|
}
|
|
27
41
|
function extractRelativeMarkdownLinks(body) {
|
|
28
42
|
const links = new Set();
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
while (
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
43
|
+
const root = unified().use(remarkParse).parse(body);
|
|
44
|
+
const pending = [root];
|
|
45
|
+
while (pending.length > 0) {
|
|
46
|
+
const node = pending.pop();
|
|
47
|
+
if (node.children) {
|
|
48
|
+
pending.push(...node.children);
|
|
49
|
+
}
|
|
50
|
+
if (!['definition', 'image', 'link'].includes(node.type ?? '') || typeof node.url !== 'string') {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const rawTarget = node.url.trim();
|
|
54
|
+
if (!rawTarget) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (rawTarget.startsWith('#')) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const scheme = rawTarget.match(/^([A-Za-z][A-Za-z0-9+.-]*):/);
|
|
61
|
+
if (scheme && scheme[1]?.toLowerCase() !== 'file') {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const withoutFragment = rawTarget.split(/[?#]/, 1)[0] ?? '';
|
|
65
|
+
if (!withoutFragment.toLowerCase().endsWith('.md')) {
|
|
34
66
|
continue;
|
|
35
67
|
}
|
|
36
|
-
|
|
68
|
+
try {
|
|
69
|
+
links.add(decodeURIComponent(withoutFragment));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
links.add(withoutFragment);
|
|
73
|
+
}
|
|
37
74
|
}
|
|
38
75
|
return [...links];
|
|
39
76
|
}
|
|
77
|
+
function countLines(content) {
|
|
78
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
79
|
+
const withoutFinalNewline = normalized.endsWith('\n') ? normalized.slice(0, -1) : normalized;
|
|
80
|
+
return withoutFinalNewline === '' ? 0 : withoutFinalNewline.split('\n').length;
|
|
81
|
+
}
|
|
82
|
+
function characterCount(value) {
|
|
83
|
+
return Array.from(value).length;
|
|
84
|
+
}
|
|
85
|
+
function normalizeTriggerQuery(value) {
|
|
86
|
+
return value.normalize('NFKC').trim().replace(/\s+/g, ' ').toLocaleLowerCase('en-US');
|
|
87
|
+
}
|
|
88
|
+
function isPathWithin(root, candidate) {
|
|
89
|
+
const relative = path.relative(root, candidate);
|
|
90
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
91
|
+
}
|
|
92
|
+
function escapeRegExp(value) {
|
|
93
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
}
|
|
40
95
|
function walkMarkdownFiles(skillDir) {
|
|
41
96
|
const files = [];
|
|
42
97
|
for (const entry of fs.readdirSync(skillDir, { withFileTypes: true })) {
|
|
@@ -156,8 +211,18 @@ function checkMarkdownDocument(skill, content, relativePath, skillDir) {
|
|
|
156
211
|
const issues = [];
|
|
157
212
|
for (const relativeTarget of extractRelativeMarkdownLinks(content)) {
|
|
158
213
|
const resolved = path.resolve(path.dirname(path.join(skillDir, relativePath)), relativeTarget);
|
|
159
|
-
if (!
|
|
214
|
+
if (!isPathWithin(path.resolve(skillDir), resolved)) {
|
|
215
|
+
issues.push({ skill, message: `${relativePath}: referenced markdown file must stay within the skill directory: ${relativeTarget}` });
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
|
|
160
219
|
issues.push({ skill, message: `${relativePath}: referenced markdown file does not exist: ${relativeTarget}` });
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const realSkillDir = fs.realpathSync(skillDir);
|
|
223
|
+
const realResolved = fs.realpathSync(resolved);
|
|
224
|
+
if (!isPathWithin(realSkillDir, realResolved)) {
|
|
225
|
+
issues.push({ skill, message: `${relativePath}: referenced markdown file resolves outside the skill directory: ${relativeTarget}` });
|
|
161
226
|
}
|
|
162
227
|
}
|
|
163
228
|
for (const command of extractDinoCommands(content)) {
|
|
@@ -167,74 +232,442 @@ function checkMarkdownDocument(skill, content, relativePath, skillDir) {
|
|
|
167
232
|
}
|
|
168
233
|
return issues;
|
|
169
234
|
}
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
235
|
+
function checkOpenAiAdapter(skill, adapterPath) {
|
|
236
|
+
const relativePath = 'agents/openai.yaml';
|
|
237
|
+
if (!fs.statSync(adapterPath).isFile()) {
|
|
238
|
+
return [{ skill, message: `${relativePath}: adapter path must be a file.` }];
|
|
239
|
+
}
|
|
240
|
+
const skillDir = path.dirname(path.dirname(adapterPath));
|
|
241
|
+
if (!isPathWithin(fs.realpathSync(skillDir), fs.realpathSync(adapterPath))) {
|
|
242
|
+
return [{ skill, message: `${relativePath}: adapter must stay within the skill directory.` }];
|
|
243
|
+
}
|
|
244
|
+
const document = parseDocument(fs.readFileSync(adapterPath, 'utf8'), {
|
|
245
|
+
prettyErrors: false,
|
|
246
|
+
uniqueKeys: true,
|
|
247
|
+
});
|
|
248
|
+
const yamlProblems = [...document.errors, ...document.warnings];
|
|
249
|
+
if (yamlProblems.length > 0) {
|
|
250
|
+
const detail = yamlProblems.map((error) => error.message).join('; ');
|
|
251
|
+
return [{ skill, message: `${relativePath}: invalid YAML: ${detail}` }];
|
|
252
|
+
}
|
|
253
|
+
if (!isMap(document.contents)) {
|
|
254
|
+
return [{ skill, message: `${relativePath}: adapter must be a YAML mapping.` }];
|
|
255
|
+
}
|
|
256
|
+
let values;
|
|
257
|
+
try {
|
|
258
|
+
const parsedValues = document.toJS({ maxAliasCount: 100 });
|
|
259
|
+
if (typeof parsedValues !== 'object' || parsedValues === null || Array.isArray(parsedValues)) {
|
|
260
|
+
return [{ skill, message: `${relativePath}: adapter must be a YAML mapping.` }];
|
|
261
|
+
}
|
|
262
|
+
values = parsedValues;
|
|
175
263
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
issues.push({ skill, message: "Frontmatter is missing 'name'." });
|
|
264
|
+
catch (error) {
|
|
265
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
266
|
+
return [{ skill, message: `${relativePath}: adapter could not be decoded: ${message}` }];
|
|
180
267
|
}
|
|
181
|
-
|
|
182
|
-
|
|
268
|
+
const issues = [];
|
|
269
|
+
const interfaceValue = values.interface;
|
|
270
|
+
if (typeof interfaceValue !== 'object' || interfaceValue === null || Array.isArray(interfaceValue)) {
|
|
271
|
+
return [{ skill, message: `${relativePath}: 'interface' must be a YAML mapping.` }];
|
|
183
272
|
}
|
|
184
|
-
|
|
185
|
-
|
|
273
|
+
const interfaceFields = interfaceValue;
|
|
274
|
+
const displayName = interfaceFields.display_name;
|
|
275
|
+
if (typeof displayName !== 'string' || displayName.trim() === '') {
|
|
276
|
+
issues.push({ skill, message: `${relativePath}: interface.display_name must be a non-empty string.` });
|
|
186
277
|
}
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
189
|
-
issues.push({ skill, message:
|
|
278
|
+
const shortDescription = interfaceFields.short_description;
|
|
279
|
+
if (typeof shortDescription !== 'string') {
|
|
280
|
+
issues.push({ skill, message: `${relativePath}: interface.short_description must be a string of 25-64 characters.` });
|
|
190
281
|
}
|
|
191
282
|
else {
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
194
|
-
issues.push({ skill, message:
|
|
283
|
+
const length = characterCount(shortDescription.trim());
|
|
284
|
+
if (length < 25 || length > 64) {
|
|
285
|
+
issues.push({ skill, message: `${relativePath}: interface.short_description must be 25-64 characters.` });
|
|
195
286
|
}
|
|
196
|
-
|
|
197
|
-
|
|
287
|
+
}
|
|
288
|
+
const defaultPrompt = interfaceFields.default_prompt;
|
|
289
|
+
if (typeof defaultPrompt !== 'string' || defaultPrompt.trim() === '') {
|
|
290
|
+
issues.push({ skill, message: `${relativePath}: interface.default_prompt must be a non-empty string.` });
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
const skillMention = new RegExp(`\\$${escapeRegExp(skill)}(?![a-z0-9-])`);
|
|
294
|
+
if (!skillMention.test(defaultPrompt)) {
|
|
295
|
+
issues.push({
|
|
296
|
+
skill,
|
|
297
|
+
message: `${relativePath}: interface.default_prompt must contain the exact skill mention '$${skill}'.`,
|
|
298
|
+
});
|
|
198
299
|
}
|
|
199
300
|
}
|
|
200
|
-
|
|
201
|
-
|
|
301
|
+
return issues;
|
|
302
|
+
}
|
|
303
|
+
function isNonEmptyString(value) {
|
|
304
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
305
|
+
}
|
|
306
|
+
function isEvalId(value) {
|
|
307
|
+
return (typeof value === 'string' && value.trim() !== '')
|
|
308
|
+
|| (typeof value === 'number' && Number.isFinite(value));
|
|
309
|
+
}
|
|
310
|
+
function checkEvaluationFileReference(skill, skillDir, value, location) {
|
|
311
|
+
if (!isNonEmptyString(value)) {
|
|
312
|
+
return [{ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location} must be a non-empty string.` }];
|
|
313
|
+
}
|
|
314
|
+
const resolved = path.resolve(skillDir, value);
|
|
315
|
+
if (!isPathWithin(path.resolve(skillDir), resolved)) {
|
|
316
|
+
return [{ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location} must stay within the skill directory: ${value}` }];
|
|
317
|
+
}
|
|
318
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
|
|
319
|
+
return [{ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location} does not exist: ${value}` }];
|
|
320
|
+
}
|
|
321
|
+
if (!isPathWithin(fs.realpathSync(skillDir), fs.realpathSync(resolved))) {
|
|
322
|
+
return [{ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location} resolves outside the skill directory: ${value}` }];
|
|
323
|
+
}
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
function checkSkillEvaluations(skill, skillDir) {
|
|
327
|
+
const manifestPath = path.join(skillDir, SKILL_EVALS_RELATIVE_PATH);
|
|
328
|
+
if (!fs.existsSync(manifestPath)) {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
let manifest;
|
|
332
|
+
try {
|
|
333
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
337
|
+
return [{ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: invalid JSON: ${message}` }];
|
|
338
|
+
}
|
|
339
|
+
if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest)) {
|
|
340
|
+
return [{ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: manifest must be a JSON object.` }];
|
|
341
|
+
}
|
|
342
|
+
const values = manifest;
|
|
343
|
+
const issues = [];
|
|
344
|
+
if (values.skill_name !== skill) {
|
|
345
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: skill_name must match directory name (${skill}).` });
|
|
346
|
+
}
|
|
347
|
+
if (values.schema_version !== 1) {
|
|
348
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: schema_version must be 1.` });
|
|
202
349
|
}
|
|
203
|
-
const
|
|
204
|
-
if (!
|
|
205
|
-
issues.push({
|
|
350
|
+
const outputEvals = values.evals;
|
|
351
|
+
if (!Array.isArray(outputEvals) || outputEvals.length < MIN_OUTPUT_EVALS) {
|
|
352
|
+
issues.push({
|
|
353
|
+
skill,
|
|
354
|
+
message: `${SKILL_EVALS_RELATIVE_PATH}: evals must contain at least ${MIN_OUTPUT_EVALS} output-quality cases.`,
|
|
355
|
+
});
|
|
206
356
|
}
|
|
207
357
|
else {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
358
|
+
const ids = new Set();
|
|
359
|
+
outputEvals.forEach((entry, index) => {
|
|
360
|
+
const location = `evals[${index}]`;
|
|
361
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
362
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location} must be an object.` });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const evalCase = entry;
|
|
366
|
+
if (!isEvalId(evalCase.id)) {
|
|
367
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location}.id must be a non-empty string or finite number.` });
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
const id = String(evalCase.id);
|
|
371
|
+
if (ids.has(id)) {
|
|
372
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: duplicate output eval id '${id}'.` });
|
|
373
|
+
}
|
|
374
|
+
ids.add(id);
|
|
375
|
+
}
|
|
376
|
+
if (!isNonEmptyString(evalCase.prompt)) {
|
|
377
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location}.prompt must be a non-empty string.` });
|
|
378
|
+
}
|
|
379
|
+
if (!isNonEmptyString(evalCase.expected_output)) {
|
|
380
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location}.expected_output must be a non-empty string.` });
|
|
381
|
+
}
|
|
382
|
+
if (evalCase.assertions !== undefined) {
|
|
383
|
+
if (!Array.isArray(evalCase.assertions) || evalCase.assertions.length === 0
|
|
384
|
+
|| evalCase.assertions.some((assertion) => !isNonEmptyString(assertion))) {
|
|
385
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location}.assertions must be a non-empty string array when provided.` });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (evalCase.files !== undefined) {
|
|
389
|
+
if (!Array.isArray(evalCase.files)) {
|
|
390
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location}.files must be an array when provided.` });
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
evalCase.files.forEach((file, fileIndex) => {
|
|
394
|
+
issues.push(...checkEvaluationFileReference(skill, skillDir, file, `${location}.files[${fileIndex}]`));
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
const triggerEvals = values.trigger_evals;
|
|
401
|
+
if (!Array.isArray(triggerEvals)) {
|
|
402
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: trigger_evals must be an array.` });
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
const ids = new Set();
|
|
406
|
+
const queries = new Set();
|
|
407
|
+
const splitCounts = new Map([
|
|
408
|
+
['train', { total: 0, positive: 0, negative: 0 }],
|
|
409
|
+
['validation', { total: 0, positive: 0, negative: 0 }],
|
|
410
|
+
]);
|
|
411
|
+
triggerEvals.forEach((entry, index) => {
|
|
412
|
+
const location = `trigger_evals[${index}]`;
|
|
413
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
414
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location} must be an object.` });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const triggerCase = entry;
|
|
418
|
+
if (!isEvalId(triggerCase.id)) {
|
|
419
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location}.id must be a non-empty string or finite number.` });
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
const id = String(triggerCase.id);
|
|
423
|
+
if (ids.has(id)) {
|
|
424
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: duplicate trigger eval id '${id}'.` });
|
|
425
|
+
}
|
|
426
|
+
ids.add(id);
|
|
427
|
+
}
|
|
428
|
+
const split = triggerCase.split;
|
|
429
|
+
if (split !== 'train' && split !== 'validation') {
|
|
430
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location}.split must be 'train' or 'validation'.` });
|
|
431
|
+
}
|
|
432
|
+
if (!isNonEmptyString(triggerCase.query)) {
|
|
433
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location}.query must be a non-empty string.` });
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
const normalizedQuery = normalizeTriggerQuery(triggerCase.query);
|
|
437
|
+
if (queries.has(normalizedQuery)) {
|
|
438
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: duplicate trigger query '${triggerCase.query.trim()}'.` });
|
|
439
|
+
}
|
|
440
|
+
queries.add(normalizedQuery);
|
|
441
|
+
}
|
|
442
|
+
if (typeof triggerCase.should_trigger !== 'boolean') {
|
|
443
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location}.should_trigger must be a boolean.` });
|
|
444
|
+
}
|
|
445
|
+
if (!isNonEmptyString(triggerCase.reason)) {
|
|
446
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location}.reason must explain the routing boundary.` });
|
|
447
|
+
}
|
|
448
|
+
const expectedSkills = triggerCase.expected_skills;
|
|
449
|
+
if (!Array.isArray(expectedSkills)
|
|
450
|
+
|| expectedSkills.some((expectedSkill) => !isNonEmptyString(expectedSkill) || !SKILL_NAME_PATTERN.test(expectedSkill))) {
|
|
451
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location}.expected_skills must be an array of portable skill names.` });
|
|
452
|
+
}
|
|
453
|
+
else if (triggerCase.should_trigger === true && !expectedSkills.includes(skill)) {
|
|
454
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location}.expected_skills must include ${skill} when should_trigger is true.` });
|
|
455
|
+
}
|
|
456
|
+
else if (triggerCase.should_trigger === false && expectedSkills.includes(skill)) {
|
|
457
|
+
issues.push({ skill, message: `${SKILL_EVALS_RELATIVE_PATH}: ${location}.expected_skills must not include ${skill} when should_trigger is false.` });
|
|
458
|
+
}
|
|
459
|
+
if ((split === 'train' || split === 'validation') && typeof triggerCase.should_trigger === 'boolean') {
|
|
460
|
+
const count = splitCounts.get(split);
|
|
461
|
+
count.total += 1;
|
|
462
|
+
if (triggerCase.should_trigger) {
|
|
463
|
+
count.positive += 1;
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
count.negative += 1;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
for (const [split, counts] of splitCounts) {
|
|
471
|
+
if (counts.total < MIN_TRIGGER_EVALS_PER_SPLIT || counts.positive === 0 || counts.negative === 0) {
|
|
472
|
+
issues.push({
|
|
473
|
+
skill,
|
|
474
|
+
message: `${SKILL_EVALS_RELATIVE_PATH}: ${split} trigger evals must contain at least ${MIN_TRIGGER_EVALS_PER_SPLIT} cases with both positive and near-miss negative examples.`,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return issues;
|
|
480
|
+
}
|
|
481
|
+
function readExpectedSkillsFromEvaluations(skillDir) {
|
|
482
|
+
const manifestPath = path.join(skillDir, SKILL_EVALS_RELATIVE_PATH);
|
|
483
|
+
try {
|
|
484
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
485
|
+
if (!Array.isArray(manifest.trigger_evals)) {
|
|
486
|
+
return [];
|
|
211
487
|
}
|
|
488
|
+
return manifest.trigger_evals.flatMap((entry) => {
|
|
489
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
490
|
+
return [];
|
|
491
|
+
}
|
|
492
|
+
const expectedSkills = entry.expected_skills;
|
|
493
|
+
return Array.isArray(expectedSkills)
|
|
494
|
+
? expectedSkills.filter(isNonEmptyString)
|
|
495
|
+
: [];
|
|
496
|
+
});
|
|
212
497
|
}
|
|
213
|
-
|
|
214
|
-
|
|
498
|
+
catch {
|
|
499
|
+
return [];
|
|
215
500
|
}
|
|
216
|
-
|
|
217
|
-
|
|
501
|
+
}
|
|
502
|
+
function readTriggerQueriesFromEvaluations(skillDir) {
|
|
503
|
+
const manifestPath = path.join(skillDir, SKILL_EVALS_RELATIVE_PATH);
|
|
504
|
+
try {
|
|
505
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
506
|
+
if (!Array.isArray(manifest.trigger_evals)) {
|
|
507
|
+
return [];
|
|
508
|
+
}
|
|
509
|
+
return manifest.trigger_evals.flatMap((entry) => {
|
|
510
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
511
|
+
return [];
|
|
512
|
+
}
|
|
513
|
+
const query = entry.query;
|
|
514
|
+
return isNonEmptyString(query) ? [query.trim()] : [];
|
|
515
|
+
});
|
|
218
516
|
}
|
|
219
|
-
|
|
220
|
-
|
|
517
|
+
catch {
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
export function checkSkillContent(skill, content, skillDir, expectedVersion) {
|
|
522
|
+
const issues = [];
|
|
523
|
+
const parsed = parseFrontmatter(content);
|
|
524
|
+
if (!parsed) {
|
|
525
|
+
return [{ skill, message: 'SKILL.md must start with valid YAML frontmatter.' }];
|
|
221
526
|
}
|
|
222
|
-
|
|
223
|
-
|
|
527
|
+
const { frontmatter, body } = parsed;
|
|
528
|
+
const document = parseDocument(frontmatter, {
|
|
529
|
+
prettyErrors: false,
|
|
530
|
+
uniqueKeys: true,
|
|
531
|
+
});
|
|
532
|
+
const yamlProblems = [...document.errors, ...document.warnings];
|
|
533
|
+
if (yamlProblems.length > 0) {
|
|
534
|
+
const detail = yamlProblems.map((error) => error.message).join('; ');
|
|
535
|
+
issues.push({ skill, message: `Frontmatter is not valid YAML: ${detail}` });
|
|
536
|
+
}
|
|
537
|
+
else if (!isMap(document.contents)) {
|
|
538
|
+
issues.push({ skill, message: 'Frontmatter must be a YAML mapping.' });
|
|
224
539
|
}
|
|
225
|
-
|
|
226
|
-
|
|
540
|
+
else {
|
|
541
|
+
let values = null;
|
|
542
|
+
try {
|
|
543
|
+
const parsedValues = document.toJS({ maxAliasCount: 100 });
|
|
544
|
+
if (typeof parsedValues === 'object' && parsedValues !== null && !Array.isArray(parsedValues)) {
|
|
545
|
+
values = parsedValues;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
550
|
+
issues.push({ skill, message: `Frontmatter could not be decoded: ${message}` });
|
|
551
|
+
}
|
|
552
|
+
for (const pair of document.contents.items) {
|
|
553
|
+
if (!isScalar(pair.key) || typeof pair.key.value !== 'string') {
|
|
554
|
+
issues.push({ skill, message: 'Frontmatter field names must be strings.' });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (values) {
|
|
558
|
+
for (const field of Object.keys(values)) {
|
|
559
|
+
if (!PORTABLE_FRONTMATTER_FIELDS.has(field)) {
|
|
560
|
+
issues.push({
|
|
561
|
+
skill,
|
|
562
|
+
message: `Unsupported portable frontmatter field '${field}'. Move vendor-specific settings to an agent adapter.`,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const name = values.name;
|
|
567
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
568
|
+
issues.push({ skill, message: "Frontmatter is missing a non-empty string 'name'." });
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
if (characterCount(name) > 64 || !SKILL_NAME_PATTERN.test(name)) {
|
|
572
|
+
issues.push({
|
|
573
|
+
skill,
|
|
574
|
+
message: "Frontmatter 'name' must be 1-64 lowercase letters, numbers, or single hyphens, without leading or trailing hyphens.",
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
if (name !== skill) {
|
|
578
|
+
issues.push({ skill, message: `Frontmatter name must match directory name (${skill}).` });
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const description = values.description;
|
|
582
|
+
if (typeof description !== 'string' || description.trim() === '') {
|
|
583
|
+
issues.push({ skill, message: "Frontmatter is missing a non-empty string 'description'." });
|
|
584
|
+
}
|
|
585
|
+
else if (characterCount(description.trim()) > 1024) {
|
|
586
|
+
issues.push({ skill, message: "Frontmatter 'description' must not exceed 1024 characters." });
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
const normalizedDescription = description.trim();
|
|
590
|
+
if (characterCount(normalizedDescription) > SKILL_DESCRIPTION_CATALOG_MAX_CHARS) {
|
|
591
|
+
issues.push({
|
|
592
|
+
skill,
|
|
593
|
+
message: `Frontmatter 'description' must stay within the ${SKILL_DESCRIPTION_CATALOG_MAX_CHARS}-character portable catalog budget.`,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (!normalizedDescription.startsWith('Use this skill')) {
|
|
597
|
+
issues.push({
|
|
598
|
+
skill,
|
|
599
|
+
message: "Frontmatter 'description' must start with imperative 'Use this skill' routing guidance.",
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
const firstSentence = normalizedDescription.match(/^.*?[.!?。!?](?:\s|$)/u)?.[0].trim();
|
|
603
|
+
if (!firstSentence
|
|
604
|
+
|| characterCount(firstSentence) > SKILL_DESCRIPTION_FIRST_SENTENCE_MAX_CHARS) {
|
|
605
|
+
issues.push({
|
|
606
|
+
skill,
|
|
607
|
+
message: `Frontmatter 'description' must start with a complete routing sentence of at most ${SKILL_DESCRIPTION_FIRST_SENTENCE_MAX_CHARS} characters for truncated catalogs.`,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (values.license !== undefined && (typeof values.license !== 'string' || values.license.trim() === '')) {
|
|
612
|
+
issues.push({ skill, message: "Frontmatter 'license' must be a non-empty string when provided." });
|
|
613
|
+
}
|
|
614
|
+
if (values.compatibility !== undefined) {
|
|
615
|
+
if (typeof values.compatibility !== 'string' || values.compatibility.trim() === '') {
|
|
616
|
+
issues.push({ skill, message: "Frontmatter 'compatibility' must be a non-empty string when provided." });
|
|
617
|
+
}
|
|
618
|
+
else if (characterCount(values.compatibility.trim()) > 500) {
|
|
619
|
+
issues.push({ skill, message: "Frontmatter 'compatibility' must not exceed 500 characters." });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (values['allowed-tools'] !== undefined
|
|
623
|
+
&& (typeof values['allowed-tools'] !== 'string' || values['allowed-tools'].trim() === '')) {
|
|
624
|
+
issues.push({ skill, message: "Frontmatter 'allowed-tools' must be a space-separated string when provided." });
|
|
625
|
+
}
|
|
626
|
+
const metadataPair = document.contents.items.find((pair) => isScalar(pair.key) && pair.key.value === 'metadata');
|
|
627
|
+
const metadata = values.metadata;
|
|
628
|
+
if (metadata !== undefined) {
|
|
629
|
+
if (!metadataPair || !isMap(metadataPair.value)) {
|
|
630
|
+
issues.push({ skill, message: "Frontmatter 'metadata' must be a string-to-string mapping when provided." });
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
for (const pair of metadataPair.value.items) {
|
|
634
|
+
if (!isScalar(pair.key) || typeof pair.key.value !== 'string') {
|
|
635
|
+
issues.push({ skill, message: "Frontmatter 'metadata' keys must be strings." });
|
|
636
|
+
}
|
|
637
|
+
if (!isScalar(pair.value) || typeof pair.value.value !== 'string') {
|
|
638
|
+
const key = isScalar(pair.key) ? String(pair.key.value) : '<unknown>';
|
|
639
|
+
issues.push({ skill, message: `Frontmatter metadata.${key} must be a string.` });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const metadataRecord = typeof metadata === 'object' && metadata !== null && !Array.isArray(metadata)
|
|
645
|
+
? metadata
|
|
646
|
+
: undefined;
|
|
647
|
+
const version = metadataRecord?.version;
|
|
648
|
+
if (version !== undefined && (typeof version !== 'string' || !SEMVER_PATTERN.test(version))) {
|
|
649
|
+
issues.push({ skill, message: 'Frontmatter metadata.version must be a semver string.' });
|
|
650
|
+
}
|
|
651
|
+
if (expectedVersion) {
|
|
652
|
+
if (version === undefined) {
|
|
653
|
+
issues.push({ skill, message: 'Frontmatter metadata.version is required for versioned Dinox skill collections.' });
|
|
654
|
+
}
|
|
655
|
+
else if (version !== expectedVersion) {
|
|
656
|
+
issues.push({ skill, message: `Frontmatter metadata.version must match skills/VERSION (${expectedVersion}).` });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
227
660
|
}
|
|
228
|
-
if (
|
|
229
|
-
issues.push({ skill, message: '
|
|
661
|
+
if (body.trim() === '') {
|
|
662
|
+
issues.push({ skill, message: 'SKILL.md must include Markdown instructions after the frontmatter.' });
|
|
230
663
|
}
|
|
231
|
-
if (
|
|
232
|
-
issues.push({ skill, message:
|
|
664
|
+
if (countLines(content) > SKILL_MAX_LINES) {
|
|
665
|
+
issues.push({ skill, message: `SKILL.md must stay at or below ${SKILL_MAX_LINES} lines; move details into references/.` });
|
|
233
666
|
}
|
|
234
|
-
if (
|
|
667
|
+
if (body.includes('dino ') && !body.includes('--format json')) {
|
|
235
668
|
issues.push({ skill, message: 'Skills that invoke dino commands must recommend --format json.' });
|
|
236
669
|
}
|
|
237
|
-
if (
|
|
670
|
+
if (body.includes('--json')) {
|
|
238
671
|
issues.push({ skill, message: 'Skills should not recommend the legacy --json flag.' });
|
|
239
672
|
}
|
|
240
673
|
if (skillDir) {
|
|
@@ -256,24 +689,59 @@ export function checkSkillsDirectory(skillsDir) {
|
|
|
256
689
|
}
|
|
257
690
|
}
|
|
258
691
|
const entries = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
259
|
-
.filter((entry) => entry.isDirectory())
|
|
692
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
260
693
|
.map((entry) => entry.name)
|
|
261
694
|
.sort();
|
|
695
|
+
const publishedSkills = new Set(entries);
|
|
696
|
+
const triggerQueryOwners = new Map();
|
|
262
697
|
for (const skill of entries) {
|
|
263
|
-
const
|
|
264
|
-
if (!fs.existsSync(
|
|
265
|
-
issues.push({ skill, message:
|
|
266
|
-
continue;
|
|
698
|
+
const skillDir = path.join(skillsDir, skill);
|
|
699
|
+
if (!fs.existsSync(path.join(skillDir, SKILL_EVALS_RELATIVE_PATH))) {
|
|
700
|
+
issues.push({ skill, message: `Missing ${SKILL_EVALS_RELATIVE_PATH}; every published skill requires eval coverage.` });
|
|
267
701
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
702
|
+
issues.push(...checkSkillDirectory(skillDir, expectedVersion));
|
|
703
|
+
for (const expectedSkill of readExpectedSkillsFromEvaluations(skillDir)) {
|
|
704
|
+
if (!publishedSkills.has(expectedSkill)) {
|
|
705
|
+
issues.push({
|
|
706
|
+
skill,
|
|
707
|
+
message: `${SKILL_EVALS_RELATIVE_PATH}: expected skill '${expectedSkill}' is not published in this collection.`,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
for (const query of readTriggerQueriesFromEvaluations(skillDir)) {
|
|
712
|
+
const normalizedQuery = normalizeTriggerQuery(query);
|
|
713
|
+
const existing = triggerQueryOwners.get(normalizedQuery);
|
|
714
|
+
if (existing) {
|
|
715
|
+
issues.push({
|
|
716
|
+
skill,
|
|
717
|
+
message: `${SKILL_EVALS_RELATIVE_PATH}: trigger query '${query}' duplicates ${existing.skill}.`,
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
triggerQueryOwners.set(normalizedQuery, { skill, query });
|
|
722
|
+
}
|
|
276
723
|
}
|
|
277
724
|
}
|
|
278
725
|
return issues;
|
|
279
726
|
}
|
|
727
|
+
export function checkSkillDirectory(skillDir, expectedVersion) {
|
|
728
|
+
const skill = path.basename(path.resolve(skillDir));
|
|
729
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
730
|
+
if (!fs.existsSync(skillFile)) {
|
|
731
|
+
return [{ skill, message: 'Missing SKILL.md.' }];
|
|
732
|
+
}
|
|
733
|
+
const content = fs.readFileSync(skillFile, 'utf8');
|
|
734
|
+
const issues = checkSkillContent(skill, content, skillDir, expectedVersion);
|
|
735
|
+
const markdownFiles = walkMarkdownFiles(skillDir).filter((file) => file !== skillFile);
|
|
736
|
+
for (const markdownFile of markdownFiles) {
|
|
737
|
+
const relativePath = path.relative(skillDir, markdownFile);
|
|
738
|
+
const markdownContent = fs.readFileSync(markdownFile, 'utf8');
|
|
739
|
+
issues.push(...checkMarkdownDocument(skill, markdownContent, relativePath, skillDir));
|
|
740
|
+
}
|
|
741
|
+
const openAiAdapterPath = path.join(skillDir, 'agents', 'openai.yaml');
|
|
742
|
+
if (fs.existsSync(openAiAdapterPath)) {
|
|
743
|
+
issues.push(...checkOpenAiAdapter(skill, openAiAdapterPath));
|
|
744
|
+
}
|
|
745
|
+
issues.push(...checkSkillEvaluations(skill, skillDir));
|
|
746
|
+
return issues;
|
|
747
|
+
}
|