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 CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/skilld?color=yellow)](https://npm.chart.dev/skilld)
5
5
  [![license](https://img.shields.io/npm/l/skilld?color=yellow)](https://github.com/harlan-zw/skilld/blob/main/LICENSE)
6
6
 
7
- > Expert SKILL.md knowledge for your NPM dependencies.
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** only add skills for packages your agent already struggles with or that you're actively debugging. Not every dependency needs a skill.
58
- - **LLM enhancement is optional** skilld generates a useful SKILL.md without any LLM, but enhancing with one makes them significantly better. This costs tokens, so be mindful.
59
- - **Multi-agent support** if you switch between agents (e.g. Claude Code and Gemini CLI), run `skilld install --agent gemini-cli` to sync your existing skills to the other agent. The doc cache is shared, so nothing is re-downloaded.
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 auto-discover from package.json
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}${state === "closed" ? `+closed:>${oneYearAgo()}` : ""}`}&sort=reactions&order=desc&per_page=${fetchCount}`,
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/main/${subdir}/README.md` : `https://ungh.cc/repos/${owner}/${repo}/readme`;
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
- for (const branch of ["main", "master"]) for (const filename of [
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}/${branch}/${basePath}${filename}`;
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
- const parts = url.replace("ungh://", "").split("/");
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/main/${subdir}/README.md` : `https://ungh.cc/repos/${owner}/${repo}/readme`, { responseType: "text" }).catch(() => null);
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+)\.(\d+)\.(\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
- if (hasMonorepoTags && packageName) {
1617
- if (!tagMatchesPackage(r.tag, packageName)) return false;
1618
- const ver = extractVersion(r.tag, packageName);
1619
- return ver && parseSemver(ver);
1620
- }
1621
- return parseSemver(r.tag);
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);