skyl.dev 0.1.2 → 0.2.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/bundle.json +1 -1
- package/dist/{bin.js → skyl.js} +157 -18
- package/package.json +3 -3
package/dist/{bin.js → skyl.js}
RENAMED
|
@@ -112,7 +112,7 @@ function list(fm, key) {
|
|
|
112
112
|
if (Array.isArray(v)) return v;
|
|
113
113
|
throw new SkillFormatError(`\`${key}\` must be a list`);
|
|
114
114
|
}
|
|
115
|
-
function parseSkill(text) {
|
|
115
|
+
function parseSkill(text, references = []) {
|
|
116
116
|
const fm = parseFrontmatter(text);
|
|
117
117
|
const md = body(text);
|
|
118
118
|
const name = requireString(fm, "name");
|
|
@@ -152,6 +152,7 @@ ${found}`;
|
|
|
152
152
|
family: name.slice(0, slash),
|
|
153
153
|
skill: name.slice(slash + 1),
|
|
154
154
|
axis,
|
|
155
|
+
description: typeof fm["description"] === "string" ? fm["description"] : "",
|
|
155
156
|
version,
|
|
156
157
|
requires: list(fm, "requires"),
|
|
157
158
|
agentSections,
|
|
@@ -161,7 +162,8 @@ ${found}`;
|
|
|
161
162
|
rules: rules(md),
|
|
162
163
|
installable,
|
|
163
164
|
installableWords: installable.split(/\s+/).filter(Boolean).length,
|
|
164
|
-
raw: text
|
|
165
|
+
raw: text,
|
|
166
|
+
references
|
|
165
167
|
};
|
|
166
168
|
}
|
|
167
169
|
|
|
@@ -303,13 +305,24 @@ function directorySource(root, label = "directory") {
|
|
|
303
305
|
} catch (cause) {
|
|
304
306
|
throw new SourceError(`cannot read ${root}`, String(cause));
|
|
305
307
|
}
|
|
308
|
+
const referencesIn = async (skillDir) => {
|
|
309
|
+
const dir = join9(skillDir, "references");
|
|
310
|
+
const names = await readdir4(dir).catch(() => []);
|
|
311
|
+
const out3 = [];
|
|
312
|
+
for (const file of names.filter((f) => f.endsWith(".md")).sort()) {
|
|
313
|
+
out3.push({ file, body: await readFile9(join9(dir, file), "utf8") });
|
|
314
|
+
}
|
|
315
|
+
return out3;
|
|
316
|
+
};
|
|
306
317
|
for (const family of families) {
|
|
307
318
|
const dir = join9(root, family);
|
|
308
319
|
const entries = (await readdir4(dir, { withFileTypes: true })).filter((e) => e.isDirectory());
|
|
309
320
|
for (const entry of entries) {
|
|
310
|
-
const
|
|
321
|
+
const skillDir = join9(dir, entry.name);
|
|
322
|
+
const file = join9(skillDir, "SKILL.md");
|
|
311
323
|
try {
|
|
312
|
-
|
|
324
|
+
const text = await readFile9(file, "utf8");
|
|
325
|
+
out2.push(parseSkill(text, await referencesIn(skillDir)));
|
|
313
326
|
} catch (cause) {
|
|
314
327
|
if (cause.code === "ENOENT") continue;
|
|
315
328
|
throw new SourceError(`cannot parse ${file}`, cause.message);
|
|
@@ -326,8 +339,9 @@ function bundleSource(load, label) {
|
|
|
326
339
|
async load() {
|
|
327
340
|
const bundle = await load();
|
|
328
341
|
return Object.entries(bundle.skills).map(([name, raw]) => {
|
|
342
|
+
const refs = Object.entries(bundle.references?.[name] ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([file, body2]) => ({ file, body: body2 }));
|
|
329
343
|
try {
|
|
330
|
-
return parseSkill(raw);
|
|
344
|
+
return parseSkill(raw, refs);
|
|
331
345
|
} catch (cause) {
|
|
332
346
|
throw new SourceError(`cannot parse \`${name}\` from ${label}`, cause.message);
|
|
333
347
|
}
|
|
@@ -358,28 +372,83 @@ async function firstAvailable(sources2) {
|
|
|
358
372
|
|
|
359
373
|
// ../core/src/targets.ts
|
|
360
374
|
var flat = (name) => name.replace("/", "-");
|
|
375
|
+
var quoted = (text) => JSON.stringify(text);
|
|
376
|
+
var takesReferences = (target) => target.fileFor("x/y").includes("/");
|
|
361
377
|
var TARGETS = [
|
|
362
378
|
{
|
|
379
|
+
// Claude Code finds a skill by reading `name` and `description` out of the
|
|
380
|
+
// frontmatter. Without them the directory is passed over silently at startup.
|
|
363
381
|
id: "claude",
|
|
364
382
|
dir: ".claude/skills",
|
|
365
|
-
fileFor: (name) => `${flat(name)}/SKILL.md
|
|
383
|
+
fileFor: (name) => `${flat(name)}/SKILL.md`,
|
|
384
|
+
header: (s) => `---
|
|
385
|
+
name: ${flat(s.name)}
|
|
386
|
+
description: ${quoted(s.description)}
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
`
|
|
366
390
|
},
|
|
367
391
|
{
|
|
392
|
+
// Cursor decides when to apply a rule from its own frontmatter, so a skill that
|
|
393
|
+
// arrives without one is either always on or never on, depending on the version.
|
|
394
|
+
// The description is what it matches against, which is why this is the description
|
|
395
|
+
// and not the name: the name says `android/core` and answers no question.
|
|
368
396
|
id: "cursor",
|
|
369
397
|
dir: ".cursor/rules",
|
|
370
398
|
fileFor: (name) => `${flat(name)}.mdc`,
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
header: (name) => `---
|
|
374
|
-
description: ${name}
|
|
399
|
+
header: (s) => `---
|
|
400
|
+
description: ${quoted(s.description)}
|
|
375
401
|
alwaysApply: false
|
|
376
402
|
---
|
|
377
403
|
|
|
378
404
|
`
|
|
379
405
|
},
|
|
380
|
-
{
|
|
381
|
-
|
|
382
|
-
|
|
406
|
+
{
|
|
407
|
+
// Windsurf reads one `trigger` field, and `model_decision` is the mode that means
|
|
408
|
+
// "read the description and decide", which is what a skill is for. It also caps a
|
|
409
|
+
// rule at 6,000 characters and the whole active set at 12,000, and enforces both by
|
|
410
|
+
// truncating rather than by saying anything.
|
|
411
|
+
id: "windsurf",
|
|
412
|
+
dir: ".windsurf/rules",
|
|
413
|
+
fileFor: (name) => `${flat(name)}.md`,
|
|
414
|
+
header: (s) => `---
|
|
415
|
+
trigger: model_decision
|
|
416
|
+
description: ${quoted(s.description)}
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
`,
|
|
420
|
+
limit: {
|
|
421
|
+
perFile: 6e3,
|
|
422
|
+
total: 12e3,
|
|
423
|
+
says: "Windsurf reads 6,000 characters of a rule and 12,000 across all of them,\nand truncates the rest without saying so."
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
id: "continue",
|
|
428
|
+
dir: ".continue/rules",
|
|
429
|
+
fileFor: (name) => `${flat(name)}.md`,
|
|
430
|
+
header: (s) => `---
|
|
431
|
+
name: ${s.name}
|
|
432
|
+
description: ${quoted(s.description)}
|
|
433
|
+
alwaysApply: false
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
`
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
// The vendor-neutral Agent Skills layout: a directory per skill holding SKILL.md,
|
|
440
|
+
// with the same two fields Claude Code reads. This was a flat file until the
|
|
441
|
+
// frontmatter went in, and a flat file has nowhere to put a skill's references.
|
|
442
|
+
id: "agents",
|
|
443
|
+
dir: ".agents/skills",
|
|
444
|
+
fileFor: (name) => `${flat(name)}/SKILL.md`,
|
|
445
|
+
header: (s) => `---
|
|
446
|
+
name: ${flat(s.name)}
|
|
447
|
+
description: ${quoted(s.description)}
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
`
|
|
451
|
+
}
|
|
383
452
|
];
|
|
384
453
|
var DEFAULT_TARGET = "claude";
|
|
385
454
|
function targetById(id) {
|
|
@@ -577,6 +646,19 @@ function lintSkill(text, path) {
|
|
|
577
646
|
});
|
|
578
647
|
}
|
|
579
648
|
}
|
|
649
|
+
if (skill.description === "") {
|
|
650
|
+
found.push({
|
|
651
|
+
level: "warn",
|
|
652
|
+
message: "no `description`",
|
|
653
|
+
detail: "nothing that lists this skill can say what it is for"
|
|
654
|
+
});
|
|
655
|
+
} else if (skill.description.length > 300) {
|
|
656
|
+
found.push({
|
|
657
|
+
level: "warn",
|
|
658
|
+
message: `description is ${String(skill.description.length)} characters`,
|
|
659
|
+
detail: "one line, read in a list next to a dozen others"
|
|
660
|
+
});
|
|
661
|
+
}
|
|
580
662
|
const md = body(text);
|
|
581
663
|
const all = sections(md);
|
|
582
664
|
const offset = text.slice(0, text.length - md.length).split("\n").length - 1;
|
|
@@ -870,10 +952,24 @@ async function writeLockfile(root, lock) {
|
|
|
870
952
|
await writeFile(join2(root, LOCKFILE_NAME), formatLockfile(lock), "utf8");
|
|
871
953
|
}
|
|
872
954
|
function renderFor(target, skill) {
|
|
873
|
-
|
|
874
|
-
return `${header}${skill.installable}
|
|
955
|
+
return `${target.header(skill)}${skill.installable}
|
|
875
956
|
`;
|
|
876
957
|
}
|
|
958
|
+
function referencePathFor(root, target, skill, file) {
|
|
959
|
+
return join2(dirname2(pathFor(root, target, skill)), "references", file);
|
|
960
|
+
}
|
|
961
|
+
function referencesDropped(target, ordered) {
|
|
962
|
+
if (takesReferences(target)) return [];
|
|
963
|
+
return ordered.filter((s) => s.references.length > 0);
|
|
964
|
+
}
|
|
965
|
+
function overLimit(target, ordered) {
|
|
966
|
+
if (!target.limit) return { over: [], total: 0 };
|
|
967
|
+
const size = (s) => renderFor(target, s).length;
|
|
968
|
+
return {
|
|
969
|
+
over: ordered.filter((s) => size(s) > target.limit.perFile),
|
|
970
|
+
total: ordered.reduce((n, s) => n + size(s), 0)
|
|
971
|
+
};
|
|
972
|
+
}
|
|
877
973
|
function pathFor(root, target, skill) {
|
|
878
974
|
return join2(root, target.dir, target.fileFor(skill.name));
|
|
879
975
|
}
|
|
@@ -890,11 +986,19 @@ async function install(root, target, ordered, requested, existing) {
|
|
|
890
986
|
target: target.id,
|
|
891
987
|
skills: { ...existing?.target === target.id ? existing.skills : {} }
|
|
892
988
|
};
|
|
989
|
+
const carriesReferences = takesReferences(target);
|
|
893
990
|
for (const skill of ordered) {
|
|
894
991
|
const file = pathFor(root, target, skill);
|
|
895
992
|
const text = renderFor(target, skill);
|
|
896
993
|
await mkdir(dirname2(file), { recursive: true });
|
|
897
994
|
await writeFile(file, text, "utf8");
|
|
995
|
+
if (carriesReferences) {
|
|
996
|
+
for (const reference of skill.references) {
|
|
997
|
+
const at = referencePathFor(root, target, skill, reference.file);
|
|
998
|
+
await mkdir(dirname2(at), { recursive: true });
|
|
999
|
+
await writeFile(at, reference.body, "utf8");
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
898
1002
|
lock.skills[skill.name] = entryFor(
|
|
899
1003
|
skill,
|
|
900
1004
|
text,
|
|
@@ -930,7 +1034,8 @@ function err(line) {
|
|
|
930
1034
|
`);
|
|
931
1035
|
}
|
|
932
1036
|
function tokens(words2) {
|
|
933
|
-
|
|
1037
|
+
const n = Math.round(words2 * 4 / 300) * 100;
|
|
1038
|
+
return `~${(words2 > 0 ? Math.max(n, 100) : 0).toLocaleString()} tokens`;
|
|
934
1039
|
}
|
|
935
1040
|
async function confirm(question, assumeYes) {
|
|
936
1041
|
if (assumeYes) return true;
|
|
@@ -965,9 +1070,36 @@ async function add(opts) {
|
|
|
965
1070
|
out(` ${bold(skill.name.padEnd(22))} ${skill.version.padEnd(8)} ${state.padEnd(18)} ${why}`);
|
|
966
1071
|
}
|
|
967
1072
|
const words2 = ordered.reduce((n, s) => n + s.installableWords, 0);
|
|
1073
|
+
const files = ordered.reduce((n, s) => n + s.references.length, 0);
|
|
1074
|
+
const alsoRefs = files > 0 && referencesDropped(target, ordered).length === 0 ? dim(`, plus ${files} reference ${files === 1 ? "file" : "files"}`) : "";
|
|
968
1075
|
out();
|
|
969
|
-
|
|
1076
|
+
const count = `${ordered.length} ${ordered.length === 1 ? "skill" : "skills"}`;
|
|
1077
|
+
out(` ${count}${alsoRefs}, ${tokens(words2)} ${dim(`into ${target.dir}`)}`);
|
|
970
1078
|
out();
|
|
1079
|
+
const dropped = referencesDropped(target, ordered);
|
|
1080
|
+
if (dropped.length > 0) {
|
|
1081
|
+
out(` ${yellow("References stay behind.")} ${dim(`${target.id} keeps one file per skill, so the`)}`);
|
|
1082
|
+
out(` ${dim("files these rules point at cannot sit beside them:")}`);
|
|
1083
|
+
for (const skill of dropped) {
|
|
1084
|
+
out(` ${dim(`${skill.name} ${skill.references.map((r) => r.file).join(", ")}`)}`);
|
|
1085
|
+
}
|
|
1086
|
+
out(` ${dim("Read them on https://skyl.dev/skills, under References.")}`);
|
|
1087
|
+
out();
|
|
1088
|
+
}
|
|
1089
|
+
const { over, total } = overLimit(target, ordered);
|
|
1090
|
+
if (target.limit && (over.length > 0 || total > target.limit.total)) {
|
|
1091
|
+
out(` ${yellow("Past what this tool reads.")}`);
|
|
1092
|
+
for (const line of target.limit.says.split("\n")) out(` ${dim(line)}`);
|
|
1093
|
+
for (const skill of over) {
|
|
1094
|
+
const size = skill.installable.length;
|
|
1095
|
+
out(` ${dim(`${skill.name.padEnd(22)} ${size.toLocaleString()} characters`)}`);
|
|
1096
|
+
}
|
|
1097
|
+
if (total > target.limit.total) {
|
|
1098
|
+
out(` ${dim(`all of them together ${total.toLocaleString()} characters`)}`);
|
|
1099
|
+
}
|
|
1100
|
+
out(` ${dim("Install fewer of them, or use a tool with no ceiling.")}`);
|
|
1101
|
+
out();
|
|
1102
|
+
}
|
|
971
1103
|
if (!await confirm(" Write them?", opts.yes === true)) {
|
|
972
1104
|
out(` ${dim("Nothing written.")}`);
|
|
973
1105
|
return 1;
|
|
@@ -1778,6 +1910,13 @@ async function lint(opts) {
|
|
|
1778
1910
|
err(`${yellow("No SKILL.md found in")} ${targets.join(", ")}`);
|
|
1779
1911
|
return 1;
|
|
1780
1912
|
}
|
|
1913
|
+
const written = await Promise.all(files.map((f) => readFile8(f, "utf8")));
|
|
1914
|
+
if (written.length > 0 && written.every((text) => /^---\nname: [^/\n]+\n/.test(text))) {
|
|
1915
|
+
err(`${yellow("That is an install directory, not a registry.")}`);
|
|
1916
|
+
err(dim(" Installed skills carry a flattened name and only the sections an agent is given."));
|
|
1917
|
+
err(dim(" Lint the registry they came from: skyl lint <checkout>/skills"));
|
|
1918
|
+
return 1;
|
|
1919
|
+
}
|
|
1781
1920
|
let errors = 0;
|
|
1782
1921
|
let warnings = 0;
|
|
1783
1922
|
const parsed = [];
|
|
@@ -2118,7 +2257,7 @@ async function recordVersions(root, lock, pairs) {
|
|
|
2118
2257
|
}
|
|
2119
2258
|
|
|
2120
2259
|
// src/cli.ts
|
|
2121
|
-
var VERSION = "0.
|
|
2260
|
+
var VERSION = "0.2.0";
|
|
2122
2261
|
var HELP = `
|
|
2123
2262
|
${bold("skyl")} ${dim("curated agent skills for what your project actually uses")}
|
|
2124
2263
|
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skyl.dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Install curated AI agent skills for what your project actually uses",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
8
|
-
"skyl": "dist/
|
|
8
|
+
"skyl": "dist/skyl.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"dist/
|
|
11
|
+
"dist/skyl.js",
|
|
12
12
|
"bundle.json",
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|