skilld 0.4.1 → 0.4.2
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 +19 -5
- package/dist/_chunks/releases.mjs +52 -26
- package/dist/_chunks/releases.mjs.map +1 -1
- package/dist/_chunks/utils.d.mts +4 -3
- package/dist/_chunks/utils.d.mts.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://npm.chart.dev/skilld)
|
|
5
5
|
[](https://github.com/harlan-zw/skilld/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
>
|
|
7
|
+
> Generate AI agent skills from your NPM dependencies.
|
|
8
8
|
|
|
9
9
|
## Why?
|
|
10
10
|
|
|
@@ -54,9 +54,9 @@ If you need to re-configure skilld, just run `npx -y skilld config` to update yo
|
|
|
54
54
|
|
|
55
55
|
### Tips
|
|
56
56
|
|
|
57
|
-
- **Be selective**
|
|
58
|
-
- **LLM
|
|
59
|
-
- **Multi-agent
|
|
57
|
+
- **Be selective** - Only add skills for packages your agent struggles with. Not every dependency needs one.
|
|
58
|
+
- **LLM is optional** - Skills work without any LLM, but enhancing with one makes them significantly better.
|
|
59
|
+
- **Multi-agent** - Run `skilld install --agent gemini-cli` to sync skills to another agent. The doc cache is shared.
|
|
60
60
|
|
|
61
61
|
## Installation
|
|
62
62
|
|
|
@@ -85,7 +85,7 @@ Add to `package.json` to keep skills fresh on install:
|
|
|
85
85
|
## CLI Usage
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
# Interactive mode
|
|
88
|
+
# Interactive mode - auto-discover from package.json
|
|
89
89
|
skilld
|
|
90
90
|
|
|
91
91
|
# Add skills for specific package(s)
|
|
@@ -147,6 +147,20 @@ skilld config
|
|
|
147
147
|
| `--prepare` | | `false` | Non-interactive sync for prepare hook (outdated only) |
|
|
148
148
|
| `--background` | `-b` | `false` | Run `--prepare` in a detached background process |
|
|
149
149
|
|
|
150
|
+
## FAQ
|
|
151
|
+
|
|
152
|
+
### How is this different from Context7?
|
|
153
|
+
|
|
154
|
+
Context7 is an MCP that fetches raw doc chunks at query time. You get different results each prompt, no curation, and it requires their server. Skilld is local-first: it generates a SKILL.md that lives in your project, tied to your actual package versions. No MCP dependency, no per-prompt latency, and it goes further with LLM-enhanced sections, prompt injection sanitization, and semantic search.
|
|
155
|
+
|
|
156
|
+
### Aren't these just AI convention files?
|
|
157
|
+
|
|
158
|
+
Similar idea, but instead of hand-writing them, skilld generates them from the latest package docs, issues, and releases. This makes them considerably more accurate at a low token cost. They also auto-update when your dependencies ship new versions.
|
|
159
|
+
|
|
160
|
+
### Do skills update when my deps update?
|
|
161
|
+
|
|
162
|
+
Yes. Run `skilld update` to regenerate outdated skills, or add `skilld --prepare -b` to your prepare script and they regenerate in the background whenever you install packages.
|
|
163
|
+
|
|
150
164
|
## Related
|
|
151
165
|
|
|
152
166
|
- [skills-npm](https://github.com/antfu/skills-npm) - Convention for shipping agent skills in npm packages
|
|
@@ -93,11 +93,22 @@ function bodyLimit(reactions) {
|
|
|
93
93
|
if (reactions >= 5) return 1500;
|
|
94
94
|
return 800;
|
|
95
95
|
}
|
|
96
|
-
function fetchIssuesByState(owner, repo, state, count) {
|
|
96
|
+
function fetchIssuesByState(owner, repo, state, count, releasedAt) {
|
|
97
97
|
const fetchCount = Math.min(count * 3, 100);
|
|
98
|
+
let datePart = "";
|
|
99
|
+
if (state === "closed") if (releasedAt) {
|
|
100
|
+
const date = new Date(releasedAt);
|
|
101
|
+
date.setMonth(date.getMonth() + 6);
|
|
102
|
+
datePart = `+closed:<=${isoDate(date.toISOString())}`;
|
|
103
|
+
} else datePart = `+closed:>${oneYearAgo()}`;
|
|
104
|
+
else if (releasedAt) {
|
|
105
|
+
const date = new Date(releasedAt);
|
|
106
|
+
date.setMonth(date.getMonth() + 6);
|
|
107
|
+
datePart = `+created:<=${isoDate(date.toISOString())}`;
|
|
108
|
+
}
|
|
98
109
|
const { stdout: result } = spawnSync("gh", [
|
|
99
110
|
"api",
|
|
100
|
-
`search/issues?q=${`repo:${owner}/${repo}+is:issue+is:${state}${
|
|
111
|
+
`search/issues?q=${`repo:${owner}/${repo}+is:issue+is:${state}${datePart}`}&sort=reactions&order=desc&per_page=${fetchCount}`,
|
|
101
112
|
"-q",
|
|
102
113
|
".items[] | {number, title, state, labels: [.labels[]?.name], body, createdAt: .created_at, url: .html_url, reactions: .reactions[\"+1\"], comments: .comments, user: .user.login, userType: .user.type}"
|
|
103
114
|
], {
|
|
@@ -148,13 +159,13 @@ function enrichWithComments(owner, repo, issues, topN = 10) {
|
|
|
148
159
|
}
|
|
149
160
|
} catch {}
|
|
150
161
|
}
|
|
151
|
-
async function fetchGitHubIssues(owner, repo, limit = 30) {
|
|
162
|
+
async function fetchGitHubIssues(owner, repo, limit = 30, releasedAt) {
|
|
152
163
|
if (!isGhAvailable()) return [];
|
|
153
164
|
const openCount = Math.ceil(limit * .75);
|
|
154
165
|
const closedCount = limit - openCount;
|
|
155
166
|
try {
|
|
156
|
-
const open = fetchIssuesByState(owner, repo, "open", openCount);
|
|
157
|
-
const closed = fetchIssuesByState(owner, repo, "closed", closedCount);
|
|
167
|
+
const open = fetchIssuesByState(owner, repo, "open", openCount, releasedAt);
|
|
168
|
+
const closed = fetchIssuesByState(owner, repo, "closed", closedCount, releasedAt);
|
|
158
169
|
const all = [...open, ...closed];
|
|
159
170
|
enrichWithComments(owner, repo, all);
|
|
160
171
|
return all;
|
|
@@ -251,8 +262,13 @@ const LOW_VALUE_CATEGORIES = new Set([
|
|
|
251
262
|
"ideas",
|
|
252
263
|
"polls"
|
|
253
264
|
]);
|
|
254
|
-
async function fetchGitHubDiscussions(owner, repo, limit = 20) {
|
|
265
|
+
async function fetchGitHubDiscussions(owner, repo, limit = 20, releasedAt) {
|
|
255
266
|
if (!isGhAvailable()) return [];
|
|
267
|
+
if (releasedAt) {
|
|
268
|
+
const cutoff = new Date(releasedAt);
|
|
269
|
+
cutoff.setMonth(cutoff.getMonth() + 6);
|
|
270
|
+
if (cutoff < /* @__PURE__ */ new Date()) return [];
|
|
271
|
+
}
|
|
256
272
|
try {
|
|
257
273
|
const { stdout: result } = spawnSync("gh", [
|
|
258
274
|
"api",
|
|
@@ -1032,16 +1048,17 @@ async function fetchGitHubRepoMeta(owner, repo, packageName) {
|
|
|
1032
1048
|
const data = await $fetch(`https://api.github.com/repos/${owner}/${repo}`).catch(() => null);
|
|
1033
1049
|
return data?.homepage ? { homepage: data.homepage } : null;
|
|
1034
1050
|
}
|
|
1035
|
-
async function fetchReadme(owner, repo, subdir) {
|
|
1036
|
-
const unghUrl = subdir ? `https://ungh.cc/repos/${owner}/${repo}/files
|
|
1037
|
-
if ((await $fetch.raw(unghUrl).catch(() => null))?.ok) return `ungh://${owner}/${repo}${subdir ? `/${subdir}` : ""}`;
|
|
1051
|
+
async function fetchReadme(owner, repo, subdir, ref) {
|
|
1052
|
+
const unghUrl = subdir ? `https://ungh.cc/repos/${owner}/${repo}/files/${ref || "main"}/${subdir}/README.md` : `https://ungh.cc/repos/${owner}/${repo}/readme${ref ? `?ref=${ref}` : ""}`;
|
|
1053
|
+
if ((await $fetch.raw(unghUrl).catch(() => null))?.ok) return `ungh://${owner}/${repo}${subdir ? `/${subdir}` : ""}${ref ? `@${ref}` : ""}`;
|
|
1038
1054
|
const basePath = subdir ? `${subdir}/` : "";
|
|
1039
|
-
|
|
1055
|
+
const branches = ref ? [ref] : ["main", "master"];
|
|
1056
|
+
for (const b of branches) for (const filename of [
|
|
1040
1057
|
"README.md",
|
|
1041
1058
|
"Readme.md",
|
|
1042
1059
|
"readme.md"
|
|
1043
1060
|
]) {
|
|
1044
|
-
const readmeUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${
|
|
1061
|
+
const readmeUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${b}/${basePath}${filename}`;
|
|
1045
1062
|
if ((await $fetch.raw(readmeUrl).catch(() => null))?.ok) return readmeUrl;
|
|
1046
1063
|
}
|
|
1047
1064
|
return null;
|
|
@@ -1053,11 +1070,18 @@ async function fetchReadmeContent(url) {
|
|
|
1053
1070
|
return readFileSync(filePath, "utf-8");
|
|
1054
1071
|
}
|
|
1055
1072
|
if (url.startsWith("ungh://")) {
|
|
1056
|
-
|
|
1073
|
+
let path = url.replace("ungh://", "");
|
|
1074
|
+
let ref = "main";
|
|
1075
|
+
const atIdx = path.lastIndexOf("@");
|
|
1076
|
+
if (atIdx !== -1) {
|
|
1077
|
+
ref = path.slice(atIdx + 1);
|
|
1078
|
+
path = path.slice(0, atIdx);
|
|
1079
|
+
}
|
|
1080
|
+
const parts = path.split("/");
|
|
1057
1081
|
const owner = parts[0];
|
|
1058
1082
|
const repo = parts[1];
|
|
1059
1083
|
const subdir = parts.slice(2).join("/");
|
|
1060
|
-
const text = await $fetch(subdir ? `https://ungh.cc/repos/${owner}/${repo}/files
|
|
1084
|
+
const text = await $fetch(subdir ? `https://ungh.cc/repos/${owner}/${repo}/files/${ref}/${subdir}/README.md` : `https://ungh.cc/repos/${owner}/${repo}/readme?ref=${ref}`, { responseType: "text" }).catch(() => null);
|
|
1061
1085
|
if (!text) return null;
|
|
1062
1086
|
try {
|
|
1063
1087
|
const json = JSON.parse(text);
|
|
@@ -1218,7 +1242,7 @@ async function resolveGitHub(gh, targetVersion, pkg, result, attempts, onProgres
|
|
|
1218
1242
|
});
|
|
1219
1243
|
}
|
|
1220
1244
|
onProgress?.("readme");
|
|
1221
|
-
const readmeUrl = await fetchReadme(gh.owner, gh.repo, opts?.subdir);
|
|
1245
|
+
const readmeUrl = await fetchReadme(gh.owner, gh.repo, opts?.subdir, result.gitRef);
|
|
1222
1246
|
if (readmeUrl) {
|
|
1223
1247
|
result.readmeUrl = readmeUrl;
|
|
1224
1248
|
attempts.push({
|
|
@@ -1475,7 +1499,7 @@ async function resolveLocalPackageDocs(localPath) {
|
|
|
1475
1499
|
result.gitDocsUrl = gitDocs.baseUrl;
|
|
1476
1500
|
result.gitRef = gitDocs.ref;
|
|
1477
1501
|
}
|
|
1478
|
-
const readmeUrl = await fetchReadme(gh.owner, gh.repo);
|
|
1502
|
+
const readmeUrl = await fetchReadme(gh.owner, gh.repo, void 0, result.gitRef);
|
|
1479
1503
|
if (readmeUrl) result.readmeUrl = readmeUrl;
|
|
1480
1504
|
}
|
|
1481
1505
|
}
|
|
@@ -1548,12 +1572,12 @@ function getInstalledSkillVersion(skillDir) {
|
|
|
1548
1572
|
}
|
|
1549
1573
|
function parseSemver(version) {
|
|
1550
1574
|
const clean = version.replace(/^v/, "");
|
|
1551
|
-
const match = clean.match(/^(\d+)
|
|
1575
|
+
const match = clean.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
1552
1576
|
if (!match) return null;
|
|
1553
1577
|
return {
|
|
1554
1578
|
major: +match[1],
|
|
1555
|
-
minor: +match[2],
|
|
1556
|
-
patch: +match[3],
|
|
1579
|
+
minor: match[2] ? +match[2] : 0,
|
|
1580
|
+
patch: match[3] ? +match[3] : 0,
|
|
1557
1581
|
raw: clean
|
|
1558
1582
|
};
|
|
1559
1583
|
}
|
|
@@ -1609,16 +1633,18 @@ async function fetchAllReleases(owner, repo) {
|
|
|
1609
1633
|
}
|
|
1610
1634
|
return fetchReleasesViaUngh(owner, repo);
|
|
1611
1635
|
}
|
|
1612
|
-
function selectReleases(releases, packageName) {
|
|
1636
|
+
function selectReleases(releases, packageName, installedVersion) {
|
|
1613
1637
|
const hasMonorepoTags = packageName && releases.some((r) => tagMatchesPackage(r.tag, packageName));
|
|
1638
|
+
const installedSv = installedVersion ? parseSemver(installedVersion) : null;
|
|
1614
1639
|
return releases.filter((r) => {
|
|
1615
1640
|
if (r.prerelease) return false;
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1641
|
+
const ver = extractVersion(r.tag, hasMonorepoTags ? packageName : void 0);
|
|
1642
|
+
if (!ver) return false;
|
|
1643
|
+
const sv = parseSemver(ver);
|
|
1644
|
+
if (!sv) return false;
|
|
1645
|
+
if (hasMonorepoTags && packageName && !tagMatchesPackage(r.tag, packageName)) return false;
|
|
1646
|
+
if (installedSv && compareSemver(sv, installedSv) > 0) return false;
|
|
1647
|
+
return true;
|
|
1622
1648
|
}).sort((a, b) => {
|
|
1623
1649
|
const verA = extractVersion(a.tag, hasMonorepoTags ? packageName : void 0);
|
|
1624
1650
|
const verB = extractVersion(b.tag, hasMonorepoTags ? packageName : void 0);
|
|
@@ -1684,7 +1710,7 @@ async function fetchChangelog(owner, repo, ref) {
|
|
|
1684
1710
|
return null;
|
|
1685
1711
|
}
|
|
1686
1712
|
async function fetchReleaseNotes(owner, repo, installedVersion, gitRef, packageName) {
|
|
1687
|
-
const selected = selectReleases(await fetchAllReleases(owner, repo), packageName);
|
|
1713
|
+
const selected = selectReleases(await fetchAllReleases(owner, repo), packageName, installedVersion);
|
|
1688
1714
|
if (selected.length > 0) {
|
|
1689
1715
|
if (isChangelogRedirectPattern(selected)) {
|
|
1690
1716
|
const changelog = await fetchChangelog(owner, repo, gitRef || selected[0].tag);
|