skilld 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.
Files changed (40) hide show
  1. package/README.md +24 -23
  2. package/dist/_chunks/config.mjs +8 -2
  3. package/dist/_chunks/config.mjs.map +1 -1
  4. package/dist/_chunks/llm.mjs +710 -204
  5. package/dist/_chunks/llm.mjs.map +1 -1
  6. package/dist/_chunks/pool.mjs +115 -0
  7. package/dist/_chunks/pool.mjs.map +1 -0
  8. package/dist/_chunks/releases.mjs +689 -179
  9. package/dist/_chunks/releases.mjs.map +1 -1
  10. package/dist/_chunks/storage.mjs +311 -19
  11. package/dist/_chunks/storage.mjs.map +1 -1
  12. package/dist/_chunks/sync-parallel.mjs +134 -378
  13. package/dist/_chunks/sync-parallel.mjs.map +1 -1
  14. package/dist/_chunks/types.d.mts +9 -6
  15. package/dist/_chunks/types.d.mts.map +1 -1
  16. package/dist/_chunks/utils.d.mts +137 -68
  17. package/dist/_chunks/utils.d.mts.map +1 -1
  18. package/dist/_chunks/version.d.mts +43 -6
  19. package/dist/_chunks/version.d.mts.map +1 -1
  20. package/dist/agent/index.d.mts +58 -15
  21. package/dist/agent/index.d.mts.map +1 -1
  22. package/dist/agent/index.mjs +4 -2
  23. package/dist/cache/index.d.mts +2 -2
  24. package/dist/cache/index.mjs +2 -2
  25. package/dist/cli.mjs +2170 -1436
  26. package/dist/cli.mjs.map +1 -1
  27. package/dist/index.d.mts +4 -3
  28. package/dist/index.mjs +2 -2
  29. package/dist/retriv/index.d.mts +16 -2
  30. package/dist/retriv/index.d.mts.map +1 -1
  31. package/dist/retriv/index.mjs +44 -15
  32. package/dist/retriv/index.mjs.map +1 -1
  33. package/dist/retriv/worker.d.mts +33 -0
  34. package/dist/retriv/worker.d.mts.map +1 -0
  35. package/dist/retriv/worker.mjs +47 -0
  36. package/dist/retriv/worker.mjs.map +1 -0
  37. package/dist/sources/index.d.mts +2 -2
  38. package/dist/sources/index.mjs +2 -2
  39. package/dist/types.d.mts +5 -3
  40. package/package.json +11 -7
@@ -1,62 +1,267 @@
1
1
  import { a as getCacheDir } from "./config.mjs";
2
- import { join, resolve } from "node:path";
3
- import { createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, unlinkSync } from "node:fs";
4
- import { execSync } from "node:child_process";
2
+ import { basename, dirname, join, resolve } from "pathe";
3
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync } from "node:fs";
4
+ import { spawnSync } from "node:child_process";
5
5
  import { globby } from "globby";
6
+ import { ofetch } from "ofetch";
7
+ import pLimit from "p-limit";
6
8
  import { Writable } from "node:stream";
7
9
  import { pathToFileURL } from "node:url";
10
+ import { resolvePathSync } from "mlly";
8
11
  let _ghAvailable;
9
12
  function isGhAvailable() {
10
13
  if (_ghAvailable !== void 0) return _ghAvailable;
11
- try {
12
- execSync("gh auth status", { stdio: "ignore" });
13
- return _ghAvailable = true;
14
- } catch {
15
- return _ghAvailable = false;
16
- }
14
+ const { status } = spawnSync("gh", ["auth", "status"], { stdio: "ignore" });
15
+ return _ghAvailable = status === 0;
17
16
  }
18
- async function fetchGitHubIssues(owner, repo, limit = 20) {
19
- if (!isGhAvailable()) return [];
17
+ const BOT_USERS = new Set([
18
+ "renovate[bot]",
19
+ "dependabot[bot]",
20
+ "renovate-bot",
21
+ "dependabot",
22
+ "github-actions[bot]"
23
+ ]);
24
+ const NOISE_LABELS = new Set([
25
+ "duplicate",
26
+ "stale",
27
+ "invalid",
28
+ "wontfix",
29
+ "won't fix",
30
+ "spam",
31
+ "off-topic",
32
+ "needs triage",
33
+ "triage"
34
+ ]);
35
+ const FEATURE_LABELS = new Set([
36
+ "enhancement",
37
+ "feature",
38
+ "feature request",
39
+ "feature-request",
40
+ "proposal",
41
+ "rfc",
42
+ "idea",
43
+ "suggestion"
44
+ ]);
45
+ const BUG_LABELS = new Set([
46
+ "bug",
47
+ "defect",
48
+ "regression",
49
+ "error",
50
+ "crash",
51
+ "fix",
52
+ "confirmed",
53
+ "verified"
54
+ ]);
55
+ const QUESTION_LABELS = new Set([
56
+ "question",
57
+ "help wanted",
58
+ "support",
59
+ "usage",
60
+ "how-to",
61
+ "help",
62
+ "assistance"
63
+ ]);
64
+ const DOCS_LABELS = new Set([
65
+ "documentation",
66
+ "docs",
67
+ "doc",
68
+ "typo"
69
+ ]);
70
+ function classifyIssue(labels) {
71
+ const lower = labels.map((l) => l.toLowerCase());
72
+ if (lower.some((l) => BUG_LABELS.has(l))) return "bug";
73
+ if (lower.some((l) => QUESTION_LABELS.has(l))) return "question";
74
+ if (lower.some((l) => DOCS_LABELS.has(l))) return "docs";
75
+ if (lower.some((l) => FEATURE_LABELS.has(l))) return "feature";
76
+ return "other";
77
+ }
78
+ function isNoiseIssue(issue) {
79
+ if (issue.labels.map((l) => l.toLowerCase()).some((l) => NOISE_LABELS.has(l))) return true;
80
+ if (issue.title.startsWith("☂️") || issue.title.startsWith("[META]") || issue.title.startsWith("[Tracking]")) return true;
81
+ return false;
82
+ }
83
+ function bodyLimit(reactions) {
84
+ if (reactions >= 10) return 2e3;
85
+ if (reactions >= 5) return 1500;
86
+ return 800;
87
+ }
88
+ function fetchIssuesByState(owner, repo, state, count) {
89
+ const fetchCount = Math.min(count * 3, 100);
90
+ const { stdout: result } = spawnSync("gh", [
91
+ "api",
92
+ `search/issues?q=${`repo:${owner}/${repo}+is:issue+is:${state}${state === "closed" ? `+closed:>${oneYearAgo()}` : ""}`}&sort=reactions&order=desc&per_page=${fetchCount}`,
93
+ "-q",
94
+ ".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}"
95
+ ], {
96
+ encoding: "utf-8",
97
+ maxBuffer: 10 * 1024 * 1024
98
+ });
99
+ if (!result) return [];
100
+ return result.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line)).filter((issue) => !BOT_USERS.has(issue.user) && issue.userType !== "Bot").filter((issue) => !isNoiseIssue(issue)).map(({ user: _, userType: __, ...issue }) => ({
101
+ ...issue,
102
+ type: classifyIssue(issue.labels),
103
+ topComments: []
104
+ })).sort((a, b) => (a.type === "feature" ? 1 : 0) - (b.type === "feature" ? 1 : 0)).slice(0, count);
105
+ }
106
+ function oneYearAgo() {
107
+ const d = /* @__PURE__ */ new Date();
108
+ d.setFullYear(d.getFullYear() - 1);
109
+ return d.toISOString().split("T")[0];
110
+ }
111
+ function enrichWithComments(owner, repo, issues, topN = 10) {
112
+ const worth = issues.filter((i) => i.comments > 0 && (i.type === "bug" || i.type === "question" || i.reactions >= 3)).sort((a, b) => b.reactions - a.reactions).slice(0, topN);
113
+ if (worth.length === 0) return;
114
+ const query = `query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { ${worth.map((issue, i) => `i${i}: issue(number: ${issue.number}) { comments(first: 3) { nodes { body author { login } reactions { totalCount } } } }`).join(" ")} } }`;
20
115
  try {
21
- const result = execSync(`gh api "repos/${owner}/${repo}/issues?per_page=${Math.min(limit * 3, 100)}&state=all" -q '.[] | {number, title, state, labels: [.labels[].name], body, createdAt: .created_at, url: .html_url, isPr: (.pull_request != null), user: .user.login, userType: .user.type}'`, {
116
+ const { stdout: result } = spawnSync("gh", [
117
+ "api",
118
+ "graphql",
119
+ "-f",
120
+ `query=${query}`,
121
+ "-f",
122
+ `owner=${owner}`,
123
+ "-f",
124
+ `repo=${repo}`
125
+ ], {
22
126
  encoding: "utf-8",
23
127
  maxBuffer: 10 * 1024 * 1024
24
128
  });
25
- const BOT_USERS = new Set([
26
- "renovate[bot]",
27
- "dependabot[bot]",
28
- "renovate-bot",
29
- "dependabot",
30
- "github-actions[bot]"
31
- ]);
32
- return result.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line)).filter((issue) => !issue.isPr && !BOT_USERS.has(issue.user) && issue.userType !== "Bot").slice(0, limit).map(({ isPr: _, user: __, userType: ___, ...issue }) => issue);
129
+ if (!result) return;
130
+ const repo_ = JSON.parse(result)?.data?.repository;
131
+ if (!repo_) return;
132
+ for (let i = 0; i < worth.length; i++) {
133
+ const nodes = repo_[`i${i}`]?.comments?.nodes;
134
+ if (!Array.isArray(nodes)) continue;
135
+ worth[i].topComments = nodes.filter((c) => c.author && !BOT_USERS.has(c.author.login)).map((c) => ({
136
+ body: c.body || "",
137
+ author: c.author.login,
138
+ reactions: c.reactions?.totalCount || 0
139
+ }));
140
+ }
141
+ } catch {}
142
+ }
143
+ async function fetchGitHubIssues(owner, repo, limit = 30) {
144
+ if (!isGhAvailable()) return [];
145
+ const openCount = Math.ceil(limit * .75);
146
+ const closedCount = limit - openCount;
147
+ try {
148
+ const open = fetchIssuesByState(owner, repo, "open", openCount);
149
+ const closed = fetchIssuesByState(owner, repo, "closed", closedCount);
150
+ const all = [...open, ...closed];
151
+ enrichWithComments(owner, repo, all);
152
+ return all;
33
153
  } catch {
34
154
  return [];
35
155
  }
36
156
  }
37
- function formatIssuesAsMarkdown(issues) {
38
- if (issues.length === 0) return "";
39
- const lines = ["# Recent Issues\n"];
40
- for (const issue of issues) {
41
- const labels = issue.labels.length > 0 ? ` [${issue.labels.join(", ")}]` : "";
42
- lines.push(`## #${issue.number}: ${issue.title}${labels}`);
43
- lines.push(`State: ${issue.state} | Created: ${issue.createdAt.split("T")[0]}`);
44
- lines.push(`URL: ${issue.url}\n`);
45
- if (issue.body) {
46
- const body = issue.body.length > 500 ? `${issue.body.slice(0, 500)}...` : issue.body;
47
- lines.push(body);
157
+ function formatIssueAsMarkdown(issue) {
158
+ const limit = bodyLimit(issue.reactions);
159
+ const fm = [
160
+ "---",
161
+ `number: ${issue.number}`,
162
+ `title: "${issue.title.replace(/"/g, "\\\"")}"`,
163
+ `type: ${issue.type}`,
164
+ `state: ${issue.state}`,
165
+ `created: ${issue.createdAt.split("T")[0]}`,
166
+ `url: ${issue.url}`,
167
+ `reactions: ${issue.reactions}`,
168
+ `comments: ${issue.comments}`
169
+ ];
170
+ if (issue.labels.length > 0) fm.push(`labels: [${issue.labels.join(", ")}]`);
171
+ fm.push("---");
172
+ const lines = [
173
+ fm.join("\n"),
174
+ "",
175
+ `# ${issue.title}`
176
+ ];
177
+ if (issue.body) {
178
+ const body = issue.body.length > limit ? `${issue.body.slice(0, limit)}...` : issue.body;
179
+ lines.push("", body);
180
+ }
181
+ if (issue.topComments.length > 0) {
182
+ lines.push("", "---", "", "## Top Comments");
183
+ for (const c of issue.topComments) {
184
+ const reactions = c.reactions > 0 ? ` (+${c.reactions})` : "";
185
+ const commentBody = c.body.length > 600 ? `${c.body.slice(0, 600)}...` : c.body;
186
+ lines.push("", `**@${c.author}**${reactions}:`, "", commentBody);
48
187
  }
49
- lines.push("\n---\n");
50
188
  }
51
189
  return lines.join("\n");
52
190
  }
191
+ function generateIssueIndex(issues) {
192
+ const byType = /* @__PURE__ */ new Map();
193
+ for (const issue of issues) {
194
+ const list = byType.get(issue.type) || [];
195
+ list.push(issue);
196
+ byType.set(issue.type, list);
197
+ }
198
+ const typeLabels = {
199
+ bug: "Bugs & Regressions",
200
+ question: "Questions & Usage Help",
201
+ docs: "Documentation",
202
+ feature: "Feature Requests",
203
+ other: "Other"
204
+ };
205
+ const typeOrder = [
206
+ "bug",
207
+ "question",
208
+ "docs",
209
+ "other",
210
+ "feature"
211
+ ];
212
+ const sections = [
213
+ [
214
+ "---",
215
+ `total: ${issues.length}`,
216
+ `open: ${issues.filter((i) => i.state === "open").length}`,
217
+ `closed: ${issues.filter((i) => i.state !== "open").length}`,
218
+ "---"
219
+ ].join("\n"),
220
+ "",
221
+ "# Issues Index",
222
+ ""
223
+ ];
224
+ for (const type of typeOrder) {
225
+ const group = byType.get(type);
226
+ if (!group?.length) continue;
227
+ sections.push(`## ${typeLabels[type]} (${group.length})`, "");
228
+ for (const issue of group) {
229
+ const reactions = issue.reactions > 0 ? ` (+${issue.reactions})` : "";
230
+ const state = issue.state === "open" ? "" : " [closed]";
231
+ sections.push(`- [#${issue.number}](./issue-${issue.number}.md): ${issue.title}${reactions}${state}`);
232
+ }
233
+ sections.push("");
234
+ }
235
+ return sections.join("\n");
236
+ }
237
+ const HIGH_VALUE_CATEGORIES = new Set([
238
+ "q&a",
239
+ "help",
240
+ "troubleshooting",
241
+ "support"
242
+ ]);
243
+ const LOW_VALUE_CATEGORIES = new Set([
244
+ "show and tell",
245
+ "ideas",
246
+ "polls"
247
+ ]);
53
248
  async function fetchGitHubDiscussions(owner, repo, limit = 20) {
54
249
  if (!isGhAvailable()) return [];
55
250
  try {
56
- const result = execSync(`gh api graphql -f query='${`query { repository(owner: "${owner}", name: "${repo}") { discussions(first: ${Math.min(limit * 2, 50)}, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { number title body category { name } createdAt url upvoteCount comments { totalCount } author { login } } } } }`}'`, {
251
+ const { stdout: result } = spawnSync("gh", [
252
+ "api",
253
+ "graphql",
254
+ "-f",
255
+ `query=${`query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { discussions(first: ${Math.min(limit * 3, 80)}, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { number title body category { name } createdAt url upvoteCount comments(first: 3) { totalCount nodes { body author { login } } } answer { body } author { login } } } } }`}`,
256
+ "-f",
257
+ `owner=${owner}`,
258
+ "-f",
259
+ `repo=${repo}`
260
+ ], {
57
261
  encoding: "utf-8",
58
262
  maxBuffer: 10 * 1024 * 1024
59
263
  });
264
+ if (!result) return [];
60
265
  const nodes = JSON.parse(result)?.data?.repository?.discussions?.nodes;
61
266
  if (!Array.isArray(nodes)) return [];
62
267
  const BOT_USERS = new Set([
@@ -66,7 +271,10 @@ async function fetchGitHubDiscussions(owner, repo, limit = 20) {
66
271
  "dependabot",
67
272
  "github-actions[bot]"
68
273
  ]);
69
- return nodes.filter((d) => d.author && !BOT_USERS.has(d.author.login)).slice(0, limit).map((d) => ({
274
+ return nodes.filter((d) => d.author && !BOT_USERS.has(d.author.login)).filter((d) => {
275
+ const cat = (d.category?.name || "").toLowerCase();
276
+ return !LOW_VALUE_CATEGORIES.has(cat);
277
+ }).map((d) => ({
70
278
  number: d.number,
71
279
  title: d.title,
72
280
  body: d.body || "",
@@ -74,33 +282,93 @@ async function fetchGitHubDiscussions(owner, repo, limit = 20) {
74
282
  createdAt: d.createdAt,
75
283
  url: d.url,
76
284
  upvoteCount: d.upvoteCount || 0,
77
- comments: d.comments?.totalCount || 0
78
- }));
285
+ comments: d.comments?.totalCount || 0,
286
+ answer: d.answer?.body || void 0,
287
+ topComments: (d.comments?.nodes || []).filter((c) => c.author && !BOT_USERS.has(c.author.login)).map((c) => ({
288
+ body: c.body || "",
289
+ author: c.author.login
290
+ }))
291
+ })).sort((a, b) => {
292
+ const aHigh = HIGH_VALUE_CATEGORIES.has(a.category.toLowerCase()) ? 1 : 0;
293
+ const bHigh = HIGH_VALUE_CATEGORIES.has(b.category.toLowerCase()) ? 1 : 0;
294
+ if (aHigh !== bHigh) return bHigh - aHigh;
295
+ return b.upvoteCount + b.comments - (a.upvoteCount + a.comments);
296
+ }).slice(0, limit);
79
297
  } catch {
80
298
  return [];
81
299
  }
82
300
  }
83
- function formatDiscussionsAsMarkdown(discussions) {
84
- if (discussions.length === 0) return "";
85
- const lines = ["# Recent Discussions\n"];
86
- for (const d of discussions) {
87
- const meta = [
88
- d.category && `Category: ${d.category}`,
89
- `Created: ${d.createdAt.split("T")[0]}`,
90
- d.upvoteCount > 0 && `Upvotes: ${d.upvoteCount}`,
91
- d.comments > 0 && `Comments: ${d.comments}`
92
- ].filter(Boolean).join(" | ");
93
- lines.push(`## #${d.number}: ${d.title}`);
94
- lines.push(meta);
95
- lines.push(`URL: ${d.url}\n`);
96
- if (d.body) {
97
- const body = d.body.length > 500 ? `${d.body.slice(0, 500)}...` : d.body;
98
- lines.push(body);
301
+ function formatDiscussionAsMarkdown(d) {
302
+ const fm = [
303
+ "---",
304
+ `number: ${d.number}`,
305
+ `title: "${d.title.replace(/"/g, "\\\"")}"`,
306
+ `category: ${d.category}`,
307
+ `created: ${d.createdAt.split("T")[0]}`,
308
+ `url: ${d.url}`,
309
+ `upvotes: ${d.upvoteCount}`,
310
+ `comments: ${d.comments}`,
311
+ `answered: ${!!d.answer}`,
312
+ "---"
313
+ ];
314
+ const bodyLimit = d.upvoteCount >= 5 ? 1500 : 800;
315
+ const lines = [
316
+ fm.join("\n"),
317
+ "",
318
+ `# ${d.title}`
319
+ ];
320
+ if (d.body) {
321
+ const body = d.body.length > bodyLimit ? `${d.body.slice(0, bodyLimit)}...` : d.body;
322
+ lines.push("", body);
323
+ }
324
+ if (d.answer) {
325
+ const answerLimit = 1e3;
326
+ const answer = d.answer.length > answerLimit ? `${d.answer.slice(0, answerLimit)}...` : d.answer;
327
+ lines.push("", "---", "", "## Accepted Answer", "", answer);
328
+ } else if (d.topComments.length > 0) {
329
+ lines.push("", "---", "", "## Top Comments");
330
+ for (const c of d.topComments) {
331
+ const commentBody = c.body.length > 600 ? `${c.body.slice(0, 600)}...` : c.body;
332
+ lines.push("", `**@${c.author}:**`, "", commentBody);
99
333
  }
100
- lines.push("\n---\n");
101
334
  }
102
335
  return lines.join("\n");
103
336
  }
337
+ function generateDiscussionIndex(discussions) {
338
+ const byCategory = /* @__PURE__ */ new Map();
339
+ for (const d of discussions) {
340
+ const cat = d.category || "Uncategorized";
341
+ const list = byCategory.get(cat) || [];
342
+ list.push(d);
343
+ byCategory.set(cat, list);
344
+ }
345
+ const answered = discussions.filter((d) => d.answer).length;
346
+ const sections = [
347
+ [
348
+ "---",
349
+ `total: ${discussions.length}`,
350
+ `answered: ${answered}`,
351
+ "---"
352
+ ].join("\n"),
353
+ "",
354
+ "# Discussions Index",
355
+ ""
356
+ ];
357
+ const cats = [...byCategory.keys()].sort((a, b) => {
358
+ return (HIGH_VALUE_CATEGORIES.has(a.toLowerCase()) ? 0 : 1) - (HIGH_VALUE_CATEGORIES.has(b.toLowerCase()) ? 0 : 1) || a.localeCompare(b);
359
+ });
360
+ for (const cat of cats) {
361
+ const group = byCategory.get(cat);
362
+ sections.push(`## ${cat} (${group.length})`, "");
363
+ for (const d of group) {
364
+ const upvotes = d.upvoteCount > 0 ? ` (+${d.upvoteCount})` : "";
365
+ const answered = d.answer ? " [answered]" : "";
366
+ sections.push(`- [#${d.number}](./discussion-${d.number}.md): ${d.title}${upvotes}${answered}`);
367
+ }
368
+ sections.push("");
369
+ }
370
+ return sections.join("\n");
371
+ }
104
372
  const SKIP_DIRS = [
105
373
  "node_modules",
106
374
  "_vendor",
@@ -153,29 +421,67 @@ async function resolveEntryFiles(packageDir) {
153
421
  }
154
422
  return entries;
155
423
  }
156
- const DOC_OVERRIDES = { vue: {
157
- owner: "vuejs",
158
- repo: "docs",
159
- path: "src",
160
- homepage: "https://vuejs.org"
161
- } };
424
+ const DOC_OVERRIDES = {
425
+ "vue": {
426
+ owner: "vuejs",
427
+ repo: "docs",
428
+ path: "src",
429
+ homepage: "https://vuejs.org"
430
+ },
431
+ "tailwindcss": {
432
+ owner: "tailwindlabs",
433
+ repo: "tailwindcss.com",
434
+ path: "src/docs",
435
+ homepage: "https://tailwindcss.com"
436
+ },
437
+ "astro": {
438
+ owner: "withastro",
439
+ repo: "docs",
440
+ path: "src/content/docs/en",
441
+ homepage: "https://docs.astro.build"
442
+ },
443
+ "@vueuse/core": {
444
+ owner: "vueuse",
445
+ repo: "vueuse",
446
+ path: "packages"
447
+ }
448
+ };
162
449
  function getDocOverride(packageName) {
163
450
  return DOC_OVERRIDES[packageName];
164
451
  }
165
- const USER_AGENT = "skilld/1.0";
452
+ const $fetch = ofetch.create({
453
+ retry: 3,
454
+ retryDelay: 500,
455
+ timeout: 15e3,
456
+ headers: { "User-Agent": "skilld/1.0" }
457
+ });
166
458
  async function fetchText(url) {
167
- const res = await fetch(url, { headers: { "User-Agent": USER_AGENT } }).catch(() => null);
168
- if (!res?.ok) return null;
169
- return res.text();
459
+ return $fetch(url, { responseType: "text" }).catch(() => null);
170
460
  }
171
461
  async function verifyUrl(url) {
172
- const res = await fetch(url, {
173
- method: "HEAD",
174
- headers: { "User-Agent": USER_AGENT }
175
- }).catch(() => null);
176
- if (!res?.ok) return false;
462
+ const res = await $fetch.raw(url, { method: "HEAD" }).catch(() => null);
463
+ if (!res) return false;
177
464
  return !(res.headers.get("content-type") || "").includes("text/html");
178
465
  }
466
+ const USELESS_HOSTS = new Set([
467
+ "twitter.com",
468
+ "x.com",
469
+ "facebook.com",
470
+ "linkedin.com",
471
+ "youtube.com",
472
+ "instagram.com",
473
+ "npmjs.com",
474
+ "www.npmjs.com",
475
+ "yarnpkg.com"
476
+ ]);
477
+ function isUselessDocsUrl(url) {
478
+ try {
479
+ const { hostname } = new URL(url);
480
+ return USELESS_HOSTS.has(hostname);
481
+ } catch {
482
+ return false;
483
+ }
484
+ }
179
485
  function isGitHubRepoUrl(url) {
180
486
  try {
181
487
  const parsed = new URL(url);
@@ -185,7 +491,7 @@ function isGitHubRepoUrl(url) {
185
491
  }
186
492
  }
187
493
  function parseGitHubUrl(url) {
188
- const match = url.match(/github\.com\/([^/]+)\/([^/]+)/);
494
+ const match = url.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[/#]|$)/);
189
495
  if (!match) return null;
190
496
  return {
191
497
  owner: match[1],
@@ -193,14 +499,21 @@ function parseGitHubUrl(url) {
193
499
  };
194
500
  }
195
501
  function normalizeRepoUrl(url) {
196
- return url.replace(/^git\+/, "").replace(/\.git$/, "").replace(/^git:\/\//, "https://").replace(/^ssh:\/\/git@github\.com/, "https://github.com");
502
+ return url.replace(/^git\+/, "").replace(/#.*$/, "").replace(/\.git$/, "").replace(/^git:\/\//, "https://").replace(/^ssh:\/\/git@github\.com/, "https://github.com").replace(/^git@github\.com:/, "https://github.com/");
503
+ }
504
+ function extractBranchHint(url) {
505
+ const hash = url.indexOf("#");
506
+ if (hash === -1) return void 0;
507
+ const fragment = url.slice(hash + 1);
508
+ if (!fragment || fragment === "readme") return void 0;
509
+ return fragment;
197
510
  }
511
+ const MIN_GIT_DOCS = 5;
512
+ const isShallowGitDocs = (n) => n > 0 && n < MIN_GIT_DOCS;
198
513
  async function listFilesAtRef(owner, repo, ref) {
199
- const res = await fetch(`https://ungh.cc/repos/${owner}/${repo}/files/${ref}`, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
200
- if (!res?.ok) return [];
201
- return (await res.json().catch(() => null))?.files?.map((f) => f.path) ?? [];
514
+ return (await $fetch(`https://ungh.cc/repos/${owner}/${repo}/files/${ref}`).catch(() => null))?.files?.map((f) => f.path) ?? [];
202
515
  }
203
- async function findGitTag(owner, repo, version, packageName) {
516
+ async function findGitTag(owner, repo, version, packageName, branchHint) {
204
517
  const candidates = [`v${version}`, version];
205
518
  if (packageName) candidates.push(`${packageName}@${version}`);
206
519
  for (const tag of candidates) {
@@ -220,7 +533,8 @@ async function findGitTag(owner, repo, version, packageName) {
220
533
  };
221
534
  }
222
535
  }
223
- for (const branch of ["main", "master"]) {
536
+ const branches = branchHint ? [branchHint, ...["main", "master"].filter((b) => b !== branchHint)] : ["main", "master"];
537
+ for (const branch of branches) {
224
538
  const files = await listFilesAtRef(owner, repo, branch);
225
539
  if (files.length > 0) return {
226
540
  ref: branch,
@@ -230,9 +544,7 @@ async function findGitTag(owner, repo, version, packageName) {
230
544
  return null;
231
545
  }
232
546
  async function findLatestReleaseTag(owner, repo, packageName) {
233
- const res = await fetch(`https://ungh.cc/repos/${owner}/${repo}/releases`, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
234
- if (!res?.ok) return null;
235
- const data = await res.json().catch(() => null);
547
+ const data = await $fetch(`https://ungh.cc/repos/${owner}/${repo}/releases`).catch(() => null);
236
548
  const prefix = `${packageName}@`;
237
549
  return data?.releases?.find((r) => r.tag.startsWith(prefix))?.tag ?? null;
238
550
  }
@@ -337,7 +649,7 @@ function discoverDocFiles(allFiles) {
337
649
  async function listDocsAtRef(owner, repo, ref, pathPrefix = "docs/") {
338
650
  return filterDocFiles(await listFilesAtRef(owner, repo, ref), pathPrefix);
339
651
  }
340
- async function fetchGitDocs(owner, repo, version, packageName) {
652
+ async function fetchGitDocs(owner, repo, version, packageName, repoUrl) {
341
653
  const override = packageName ? getDocOverride(packageName) : void 0;
342
654
  if (override) {
343
655
  const ref = override.ref || "main";
@@ -349,15 +661,17 @@ async function fetchGitDocs(owner, repo, version, packageName) {
349
661
  files
350
662
  };
351
663
  }
352
- const tag = await findGitTag(owner, repo, version, packageName);
664
+ const tag = await findGitTag(owner, repo, version, packageName, repoUrl ? extractBranchHint(repoUrl) : void 0);
353
665
  if (!tag) return null;
354
666
  let docs = filterDocFiles(tag.files, "docs/");
355
667
  let docsPrefix;
668
+ let allFiles;
356
669
  if (docs.length === 0) {
357
670
  const discovered = discoverDocFiles(tag.files);
358
671
  if (discovered) {
359
672
  docs = discovered.files;
360
673
  docsPrefix = discovered.prefix || void 0;
674
+ allFiles = tag.files;
361
675
  }
362
676
  }
363
677
  if (docs.length === 0) return null;
@@ -365,51 +679,127 @@ async function fetchGitDocs(owner, repo, version, packageName) {
365
679
  baseUrl: `https://raw.githubusercontent.com/${owner}/${repo}/${tag.ref}`,
366
680
  ref: tag.ref,
367
681
  files: docs,
368
- docsPrefix
682
+ docsPrefix,
683
+ allFiles
684
+ };
685
+ }
686
+ function normalizePath(p) {
687
+ return p.replace(/^\//, "").replace(/\.(?:md|mdx)$/, "");
688
+ }
689
+ function validateGitDocsWithLlms(llmsLinks, repoFiles) {
690
+ if (llmsLinks.length === 0) return {
691
+ isValid: true,
692
+ matchRatio: 1
693
+ };
694
+ const sample = llmsLinks.slice(0, 10);
695
+ const normalizedLinks = sample.map((link) => {
696
+ let path = link.url;
697
+ if (path.startsWith("http")) try {
698
+ path = new URL(path).pathname;
699
+ } catch {}
700
+ return normalizePath(path);
701
+ });
702
+ const repoNormalized = new Set(repoFiles.map(normalizePath));
703
+ let matches = 0;
704
+ for (const linkPath of normalizedLinks) for (const repoPath of repoNormalized) if (repoPath === linkPath || repoPath.endsWith(`/${linkPath}`)) {
705
+ matches++;
706
+ break;
707
+ }
708
+ const matchRatio = matches / sample.length;
709
+ return {
710
+ isValid: matchRatio >= .3,
711
+ matchRatio
369
712
  };
370
713
  }
714
+ async function verifyNpmRepo(owner, repo, packageName) {
715
+ const base = `https://raw.githubusercontent.com/${owner}/${repo}/HEAD`;
716
+ const paths = [
717
+ "package.json",
718
+ `packages/${packageName.replace(/^@.*\//, "")}/package.json`,
719
+ `packages/${packageName.replace(/^@/, "").replace("/", "-")}/package.json`
720
+ ];
721
+ for (const path of paths) {
722
+ const text = await fetchText(`${base}/${path}`);
723
+ if (!text) continue;
724
+ try {
725
+ if (JSON.parse(text).name === packageName) return true;
726
+ } catch {}
727
+ }
728
+ return false;
729
+ }
371
730
  async function searchGitHubRepo(packageName) {
731
+ const shortName = packageName.replace(/^@.*\//, "");
732
+ for (const candidate of [packageName.replace(/^@/, "").replace("/", "/"), shortName]) {
733
+ if (!candidate.includes("/")) {
734
+ if ((await $fetch.raw(`https://ungh.cc/repos/${shortName}/${shortName}`).catch(() => null))?.ok) return `https://github.com/${shortName}/${shortName}`;
735
+ continue;
736
+ }
737
+ if ((await $fetch.raw(`https://ungh.cc/repos/${candidate}`).catch(() => null))?.ok) return `https://github.com/${candidate}`;
738
+ }
739
+ const searchTerm = packageName.replace(/^@/, "");
372
740
  if (isGhAvailable()) try {
373
- const json = execSync(`gh search repos "${packageName}" --json fullName --limit 5`, {
741
+ const { stdout: json } = spawnSync("gh", [
742
+ "search",
743
+ "repos",
744
+ searchTerm,
745
+ "--json",
746
+ "fullName",
747
+ "--limit",
748
+ "5"
749
+ ], {
374
750
  encoding: "utf-8",
375
751
  timeout: 15e3
376
752
  });
753
+ if (!json) throw new Error("no output");
377
754
  const repos = JSON.parse(json);
378
- const match = repos.find((r) => r.fullName.toLowerCase().endsWith(`/${packageName.toLowerCase()}`) || r.fullName.toLowerCase().endsWith(`/${packageName.replace(/^@.*\//, "").toLowerCase()}`));
755
+ const match = repos.find((r) => r.fullName.toLowerCase().endsWith(`/${packageName.toLowerCase()}`) || r.fullName.toLowerCase().endsWith(`/${shortName.toLowerCase()}`));
379
756
  if (match) return `https://github.com/${match.fullName}`;
380
- if (repos.length > 0) return `https://github.com/${repos[0].fullName}`;
757
+ for (const candidate of repos) {
758
+ const gh = parseGitHubUrl(`https://github.com/${candidate.fullName}`);
759
+ if (gh && await verifyNpmRepo(gh.owner, gh.repo, packageName)) return `https://github.com/${candidate.fullName}`;
760
+ }
381
761
  } catch {}
382
- const query = encodeURIComponent(`${packageName} in:name`);
383
- const res = await fetch(`https://api.github.com/search/repositories?q=${query}&per_page=5`, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
384
- if (!res?.ok) return null;
385
- const data = await res.json().catch(() => null);
762
+ const data = await $fetch(`https://api.github.com/search/repositories?q=${encodeURIComponent(`${searchTerm} in:name`)}&per_page=5`).catch(() => null);
386
763
  if (!data?.items?.length) return null;
387
- const match = data.items.find((r) => r.full_name.toLowerCase().endsWith(`/${packageName.toLowerCase()}`) || r.full_name.toLowerCase().endsWith(`/${packageName.replace(/^@.*\//, "").toLowerCase()}`));
388
- return match ? `https://github.com/${match.full_name}` : `https://github.com/${data.items[0].full_name}`;
764
+ const match = data.items.find((r) => r.full_name.toLowerCase().endsWith(`/${packageName.toLowerCase()}`) || r.full_name.toLowerCase().endsWith(`/${shortName.toLowerCase()}`));
765
+ if (match) return `https://github.com/${match.full_name}`;
766
+ for (const candidate of data.items) {
767
+ const gh = parseGitHubUrl(`https://github.com/${candidate.full_name}`);
768
+ if (gh && await verifyNpmRepo(gh.owner, gh.repo, packageName)) return `https://github.com/${candidate.full_name}`;
769
+ }
770
+ return null;
389
771
  }
390
772
  async function fetchGitHubRepoMeta(owner, repo, packageName) {
391
773
  const override = packageName ? getDocOverride(packageName) : void 0;
392
774
  if (override?.homepage) return { homepage: override.homepage };
393
775
  if (isGhAvailable()) try {
394
- const json = execSync(`gh api "repos/${owner}/${repo}" -q '{homepage}'`, {
776
+ const { stdout: json } = spawnSync("gh", [
777
+ "api",
778
+ `repos/${owner}/${repo}`,
779
+ "-q",
780
+ "{homepage}"
781
+ ], {
395
782
  encoding: "utf-8",
396
783
  timeout: 1e4
397
784
  });
785
+ if (!json) throw new Error("no output");
398
786
  const data = JSON.parse(json);
399
787
  return data?.homepage ? { homepage: data.homepage } : null;
400
788
  } catch {}
401
- const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
402
- if (!res?.ok) return null;
403
- const data = await res.json().catch(() => null);
789
+ const data = await $fetch(`https://api.github.com/repos/${owner}/${repo}`).catch(() => null);
404
790
  return data?.homepage ? { homepage: data.homepage } : null;
405
791
  }
406
792
  async function fetchReadme(owner, repo, subdir) {
407
793
  const unghUrl = subdir ? `https://ungh.cc/repos/${owner}/${repo}/files/main/${subdir}/README.md` : `https://ungh.cc/repos/${owner}/${repo}/readme`;
408
- if ((await fetch(unghUrl, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null))?.ok) return `ungh://${owner}/${repo}${subdir ? `/${subdir}` : ""}`;
794
+ if ((await $fetch.raw(unghUrl).catch(() => null))?.ok) return `ungh://${owner}/${repo}${subdir ? `/${subdir}` : ""}`;
409
795
  const basePath = subdir ? `${subdir}/` : "";
410
- for (const branch of ["main", "master"]) for (const filename of ["README.md", "readme.md"]) {
796
+ for (const branch of ["main", "master"]) for (const filename of [
797
+ "README.md",
798
+ "Readme.md",
799
+ "readme.md"
800
+ ]) {
411
801
  const readmeUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${basePath}${filename}`;
412
- if (await verifyUrl(readmeUrl)) return readmeUrl;
802
+ if ((await $fetch.raw(readmeUrl).catch(() => null))?.ok) return readmeUrl;
413
803
  }
414
804
  return null;
415
805
  }
@@ -426,10 +816,8 @@ async function fetchReadmeContent(url) {
426
816
  const owner = parts[0];
427
817
  const repo = parts[1];
428
818
  const subdir = parts.slice(2).join("/");
429
- const unghUrl = subdir ? `https://ungh.cc/repos/${owner}/${repo}/files/main/${subdir}/README.md` : `https://ungh.cc/repos/${owner}/${repo}/readme`;
430
- const res = await fetch(unghUrl, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
431
- if (!res?.ok) return null;
432
- const text = await res.text();
819
+ 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);
820
+ if (!text) return null;
433
821
  try {
434
822
  const json = JSON.parse(text);
435
823
  return json.markdown || json.file?.contents || null;
@@ -440,7 +828,7 @@ async function fetchReadmeContent(url) {
440
828
  return fetchText(url);
441
829
  }
442
830
  async function fetchLlmsUrl(docsUrl) {
443
- const llmsUrl = `${docsUrl.replace(/\/$/, "")}/llms.txt`;
831
+ const llmsUrl = `${new URL(docsUrl).origin}/llms.txt`;
444
832
  if (await verifyUrl(llmsUrl)) return llmsUrl;
445
833
  return null;
446
834
  }
@@ -468,19 +856,35 @@ function parseMarkdownLinks(content) {
468
856
  }
469
857
  return links;
470
858
  }
859
+ function isSafeUrl(url) {
860
+ try {
861
+ const parsed = new URL(url);
862
+ if (parsed.protocol !== "https:") return false;
863
+ const host = parsed.hostname;
864
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1") return false;
865
+ if (host === "169.254.169.254") return false;
866
+ if (/^(?:10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.)/.test(host)) return false;
867
+ if (host.startsWith("[")) return false;
868
+ return true;
869
+ } catch {
870
+ return false;
871
+ }
872
+ }
471
873
  async function downloadLlmsDocs(llmsContent, baseUrl, onProgress) {
472
- const docs = [];
473
- for (let i = 0; i < llmsContent.links.length; i++) {
474
- const link = llmsContent.links[i];
475
- onProgress?.(link.url, i, llmsContent.links.length);
476
- const content = await fetchText(link.url.startsWith("http") ? link.url : `${baseUrl.replace(/\/$/, "")}${link.url.startsWith("/") ? "" : "/"}${link.url}`);
477
- if (content && content.length > 100) docs.push({
874
+ const limit = pLimit(5);
875
+ let completed = 0;
876
+ return (await Promise.all(llmsContent.links.map((link) => limit(async () => {
877
+ const url = link.url.startsWith("http") ? link.url : `${baseUrl.replace(/\/$/, "")}${link.url.startsWith("/") ? "" : "/"}${link.url}`;
878
+ if (!isSafeUrl(url)) return null;
879
+ onProgress?.(link.url, completed++, llmsContent.links.length);
880
+ const content = await fetchText(url);
881
+ if (content && content.length > 100) return {
478
882
  url: link.url,
479
883
  title: link.title,
480
884
  content
481
- });
482
- }
483
- return docs;
885
+ };
886
+ return null;
887
+ })))).filter((d) => d !== null);
484
888
  }
485
889
  function normalizeLlmsLinks(content, baseUrl) {
486
890
  let normalized = content;
@@ -506,16 +910,23 @@ function extractSections(content, patterns) {
506
910
  if (sections.length === 0) return null;
507
911
  return sections.join("\n\n---\n\n");
508
912
  }
913
+ async function searchNpmPackages(query, size = 5) {
914
+ const data = await $fetch(`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=${size}`).catch(() => null);
915
+ if (!data?.objects?.length) return [];
916
+ return data.objects.map((o) => ({
917
+ name: o.package.name,
918
+ description: o.package.description,
919
+ version: o.package.version
920
+ }));
921
+ }
509
922
  async function fetchNpmPackage(packageName) {
510
- let res = await fetch(`https://unpkg.com/${packageName}/package.json`, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
511
- if (!res?.ok) res = await fetch(`https://registry.npmjs.org/${packageName}/latest`, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
512
- if (!res?.ok) return null;
513
- return res.json();
923
+ const data = await $fetch(`https://unpkg.com/${packageName}/package.json`).catch(() => null);
924
+ if (data) return data;
925
+ return $fetch(`https://registry.npmjs.org/${packageName}/latest`).catch(() => null);
514
926
  }
515
927
  async function fetchNpmRegistryMeta(packageName, version) {
516
- const res = await fetch(`https://registry.npmjs.org/${packageName}`, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
517
- if (!res?.ok) return {};
518
- const data = await res.json();
928
+ const data = await $fetch(`https://registry.npmjs.org/${packageName}`).catch(() => null);
929
+ if (!data) return {};
519
930
  const distTags = data["dist-tags"] ? Object.fromEntries(Object.entries(data["dist-tags"]).map(([tag, ver]) => [tag, {
520
931
  version: ver,
521
932
  releasedAt: data.time?.[ver]
@@ -560,24 +971,34 @@ async function resolvePackageDocsWithAttempts(packageName, options = {}) {
560
971
  dependencies: pkg.dependencies,
561
972
  distTags: registryMeta.distTags
562
973
  };
974
+ let gitDocsAllFiles;
563
975
  let subdir;
976
+ let rawRepoUrl;
564
977
  if (typeof pkg.repository === "object" && pkg.repository?.url) {
565
- result.repoUrl = normalizeRepoUrl(pkg.repository.url);
978
+ rawRepoUrl = pkg.repository.url;
979
+ const normalized = normalizeRepoUrl(rawRepoUrl);
980
+ if (!normalized.includes("://") && normalized.includes("/") && !normalized.includes(":")) result.repoUrl = `https://github.com/${normalized}`;
981
+ else result.repoUrl = normalized;
566
982
  subdir = pkg.repository.directory;
567
- } else if (typeof pkg.repository === "string") {
983
+ } else if (typeof pkg.repository === "string") if (pkg.repository.includes("://")) {
984
+ const gh = parseGitHubUrl(pkg.repository);
985
+ if (gh) result.repoUrl = `https://github.com/${gh.owner}/${gh.repo}`;
986
+ } else {
568
987
  const repo = pkg.repository.replace(/^github:/, "");
569
988
  if (repo.includes("/") && !repo.includes(":")) result.repoUrl = `https://github.com/${repo}`;
570
989
  }
990
+ if (pkg.homepage && !isGitHubRepoUrl(pkg.homepage) && !isUselessDocsUrl(pkg.homepage)) result.docsUrl = pkg.homepage;
571
991
  if (result.repoUrl?.includes("github.com")) {
572
992
  const gh = parseGitHubUrl(result.repoUrl);
573
993
  if (gh) {
574
994
  const targetVersion = options.version || pkg.version;
575
995
  if (targetVersion) {
576
996
  onProgress?.("github-docs");
577
- const gitDocs = await fetchGitDocs(gh.owner, gh.repo, targetVersion, pkg.name);
997
+ const gitDocs = await fetchGitDocs(gh.owner, gh.repo, targetVersion, pkg.name, rawRepoUrl);
578
998
  if (gitDocs) {
579
999
  result.gitDocsUrl = gitDocs.baseUrl;
580
1000
  result.gitRef = gitDocs.ref;
1001
+ gitDocsAllFiles = gitDocs.allFiles;
581
1002
  attempts.push({
582
1003
  source: "github-docs",
583
1004
  url: gitDocs.baseUrl,
@@ -594,7 +1015,7 @@ async function resolvePackageDocsWithAttempts(packageName, options = {}) {
594
1015
  if (!result.docsUrl) {
595
1016
  onProgress?.("github-meta");
596
1017
  const repoMeta = await fetchGitHubRepoMeta(gh.owner, gh.repo, pkg.name);
597
- if (repoMeta?.homepage) {
1018
+ if (repoMeta?.homepage && !isUselessDocsUrl(repoMeta.homepage)) {
598
1019
  result.docsUrl = repoMeta.homepage;
599
1020
  attempts.push({
600
1021
  source: "github-meta",
@@ -645,6 +1066,7 @@ async function resolvePackageDocsWithAttempts(packageName, options = {}) {
645
1066
  if (gitDocs) {
646
1067
  result.gitDocsUrl = gitDocs.baseUrl;
647
1068
  result.gitRef = gitDocs.ref;
1069
+ gitDocsAllFiles = gitDocs.allFiles;
648
1070
  attempts.push({
649
1071
  source: "github-docs",
650
1072
  url: gitDocs.baseUrl,
@@ -656,7 +1078,7 @@ async function resolvePackageDocsWithAttempts(packageName, options = {}) {
656
1078
  if (!result.docsUrl) {
657
1079
  onProgress?.("github-meta");
658
1080
  const repoMeta = await fetchGitHubRepoMeta(gh.owner, gh.repo, pkg.name);
659
- if (repoMeta?.homepage) result.docsUrl = repoMeta.homepage;
1081
+ if (repoMeta?.homepage && !isUselessDocsUrl(repoMeta.homepage)) result.docsUrl = repoMeta.homepage;
660
1082
  }
661
1083
  onProgress?.("readme");
662
1084
  const readmeUrl = await fetchReadme(gh.owner, gh.repo);
@@ -668,7 +1090,6 @@ async function resolvePackageDocsWithAttempts(packageName, options = {}) {
668
1090
  message: "No repository URL in package.json and GitHub search found no match"
669
1091
  });
670
1092
  }
671
- if (pkg.homepage && !isGitHubRepoUrl(pkg.homepage)) result.docsUrl = pkg.homepage;
672
1093
  if (result.docsUrl) {
673
1094
  onProgress?.("llms.txt");
674
1095
  const llmsUrl = await fetchLlmsUrl(result.docsUrl);
@@ -681,28 +1102,42 @@ async function resolvePackageDocsWithAttempts(packageName, options = {}) {
681
1102
  });
682
1103
  } else attempts.push({
683
1104
  source: "llms.txt",
684
- url: `${result.docsUrl}/llms.txt`,
1105
+ url: `${new URL(result.docsUrl).origin}/llms.txt`,
685
1106
  status: "not-found",
686
1107
  message: "No llms.txt at docs URL"
687
1108
  });
688
1109
  }
689
- if (!result.docsUrl && !result.llmsUrl && !result.readmeUrl && !result.gitDocsUrl && options.cwd) {
690
- onProgress?.("local");
691
- const pkgDir = join(options.cwd, "node_modules", packageName);
692
- for (const filename of ["README.md", "readme.md"]) {
693
- const readmePath = join(pkgDir, filename);
694
- if (existsSync(readmePath)) {
695
- result.readmeUrl = pathToFileURL(readmePath).href;
1110
+ if (result.gitDocsUrl && result.llmsUrl && gitDocsAllFiles) {
1111
+ const llmsContent = await fetchLlmsTxt(result.llmsUrl);
1112
+ if (llmsContent && llmsContent.links.length > 0) {
1113
+ const validation = validateGitDocsWithLlms(llmsContent.links, gitDocsAllFiles);
1114
+ if (!validation.isValid) {
696
1115
  attempts.push({
697
- source: "readme",
698
- url: readmePath,
699
- status: "success",
700
- message: "Found local readme in node_modules"
1116
+ source: "github-docs",
1117
+ url: result.gitDocsUrl,
1118
+ status: "not-found",
1119
+ message: `Heuristic git docs don't match llms.txt links (${Math.round(validation.matchRatio * 100)}% match), preferring llms.txt`
701
1120
  });
702
- break;
1121
+ result.gitDocsUrl = void 0;
1122
+ result.gitRef = void 0;
703
1123
  }
704
1124
  }
705
1125
  }
1126
+ if (!result.docsUrl && !result.llmsUrl && !result.readmeUrl && !result.gitDocsUrl && options.cwd) {
1127
+ onProgress?.("local");
1128
+ const pkgDir = join(options.cwd, "node_modules", packageName);
1129
+ const readmeFile = existsSync(pkgDir) && readdirSync(pkgDir).find((f) => /^readme\.md$/i.test(f));
1130
+ if (readmeFile) {
1131
+ const readmePath = join(pkgDir, readmeFile);
1132
+ result.readmeUrl = pathToFileURL(readmePath).href;
1133
+ attempts.push({
1134
+ source: "readme",
1135
+ url: readmePath,
1136
+ status: "success",
1137
+ message: "Found local readme in node_modules"
1138
+ });
1139
+ }
1140
+ }
706
1141
  if (!result.docsUrl && !result.llmsUrl && !result.readmeUrl && !result.gitDocsUrl) return {
707
1142
  package: null,
708
1143
  attempts
@@ -724,27 +1159,46 @@ function parseVersionSpecifier(name, version, cwd) {
724
1159
  }
725
1160
  return null;
726
1161
  }
727
- if (version.startsWith("workspace:")) return {
728
- name,
729
- version: version.slice(10).replace(/^[\^~*]/, "") || "*"
730
- };
731
1162
  if (version.startsWith("npm:")) {
732
1163
  const specifier = version.slice(4);
733
1164
  const atIndex = specifier.startsWith("@") ? specifier.indexOf("@", 1) : specifier.indexOf("@");
734
- if (atIndex > 0) return {
735
- name: specifier.slice(0, atIndex),
736
- version: specifier.slice(atIndex + 1)
737
- };
1165
+ const realName = atIndex > 0 ? specifier.slice(0, atIndex) : specifier;
738
1166
  return {
739
- name: specifier,
740
- version: "*"
1167
+ name: realName,
1168
+ version: resolveInstalledVersion(realName, cwd) || "*"
741
1169
  };
742
1170
  }
743
1171
  if (version.startsWith("file:") || version.startsWith("git:") || version.startsWith("git+")) return null;
744
- return {
1172
+ const installed = resolveInstalledVersion(name, cwd);
1173
+ if (installed) return {
1174
+ name,
1175
+ version: installed
1176
+ };
1177
+ if (/^[\^~>=<\d]/.test(version)) return {
745
1178
  name,
746
1179
  version: version.replace(/^[\^~>=<]/, "")
747
1180
  };
1181
+ if (version.startsWith("catalog:") || version.startsWith("workspace:")) return {
1182
+ name,
1183
+ version: "*"
1184
+ };
1185
+ return null;
1186
+ }
1187
+ function resolveInstalledVersion(name, cwd) {
1188
+ try {
1189
+ const resolved = resolvePathSync(`${name}/package.json`, { url: cwd });
1190
+ return JSON.parse(readFileSync(resolved, "utf-8")).version || null;
1191
+ } catch {
1192
+ try {
1193
+ let dir = dirname(resolvePathSync(name, { url: cwd }));
1194
+ while (dir && basename(dir) !== "node_modules") {
1195
+ const pkgPath = join(dir, "package.json");
1196
+ if (existsSync(pkgPath)) return JSON.parse(readFileSync(pkgPath, "utf-8")).version || null;
1197
+ dir = dirname(dir);
1198
+ }
1199
+ } catch {}
1200
+ return null;
1201
+ }
748
1202
  }
749
1203
  async function readLocalDependencies(cwd) {
750
1204
  const pkgPath = join(cwd, "package.json");
@@ -805,8 +1259,8 @@ async function resolveLocalPackageDocs(localPath) {
805
1259
  }
806
1260
  }
807
1261
  if (!result.readmeUrl && !result.gitDocsUrl) {
808
- const localReadme = join(localPath, "README.md");
809
- if (existsSync(localReadme)) result.readmeUrl = pathToFileURL(localReadme).href;
1262
+ const readmeFile = readdirSync(localPath).find((f) => /^readme\.md$/i.test(f));
1263
+ if (readmeFile) result.readmeUrl = pathToFileURL(join(localPath, readmeFile)).href;
810
1264
  }
811
1265
  if (!result.readmeUrl && !result.gitDocsUrl) return null;
812
1266
  return result;
@@ -815,9 +1269,9 @@ async function fetchPkgDist(name, version) {
815
1269
  const cacheDir = getCacheDir(name, version);
816
1270
  const pkgDir = join(cacheDir, "pkg");
817
1271
  if (existsSync(join(pkgDir, "package.json"))) return pkgDir;
818
- const res = await fetch(`https://registry.npmjs.org/${name}/${version}`, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
819
- if (!res?.ok) return null;
820
- const tarballUrl = (await res.json()).dist?.tarball;
1272
+ const data = await $fetch(`https://registry.npmjs.org/${name}/${version}`).catch(() => null);
1273
+ if (!data) return null;
1274
+ const tarballUrl = data.dist?.tarball;
821
1275
  if (!tarballUrl) return null;
822
1276
  const tarballRes = await fetch(tarballUrl, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
823
1277
  if (!tarballRes?.ok || !tarballRes.body) return null;
@@ -845,9 +1299,14 @@ async function fetchPkgDist(name, version) {
845
1299
  }
846
1300
  pump();
847
1301
  });
848
- try {
849
- execSync(`tar xzf "${tmpTarball}" --strip-components=1 -C "${pkgDir}"`, { stdio: "ignore" });
850
- } catch {
1302
+ const { status } = spawnSync("tar", [
1303
+ "xzf",
1304
+ tmpTarball,
1305
+ "--strip-components=1",
1306
+ "-C",
1307
+ pkgDir
1308
+ ], { stdio: "ignore" });
1309
+ if (status !== 0) {
851
1310
  rmSync(pkgDir, {
852
1311
  recursive: true,
853
1312
  force: true
@@ -859,9 +1318,7 @@ async function fetchPkgDist(name, version) {
859
1318
  return pkgDir;
860
1319
  }
861
1320
  async function fetchLatestVersion(packageName) {
862
- const res = await fetch(`https://unpkg.com/${packageName}/package.json`, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
863
- if (!res?.ok) return null;
864
- return (await res.json()).version || null;
1321
+ return (await $fetch(`https://unpkg.com/${packageName}/package.json`).catch(() => null))?.version || null;
865
1322
  }
866
1323
  function getInstalledSkillVersion(skillDir) {
867
1324
  const skillPath = join(skillDir, "SKILL.md");
@@ -901,7 +1358,12 @@ function compareSemver(a, b) {
901
1358
  }
902
1359
  function fetchReleasesViaGh(owner, repo) {
903
1360
  try {
904
- const json = execSync(`gh api "repos/${owner}/${repo}/releases?per_page=100" --jq '[.[] | {id: .id, tag: .tag_name, name: .name, prerelease: .prerelease, createdAt: .created_at, publishedAt: .published_at, markdown: .body}]'`, {
1361
+ const { stdout: json } = spawnSync("gh", [
1362
+ "api",
1363
+ `repos/${owner}/${repo}/releases?per_page=100`,
1364
+ "--jq",
1365
+ "[.[] | {id: .id, tag: .tag_name, name: .name, prerelease: .prerelease, createdAt: .created_at, publishedAt: .published_at, markdown: .body}]"
1366
+ ], {
905
1367
  encoding: "utf-8",
906
1368
  timeout: 15e3,
907
1369
  stdio: [
@@ -910,18 +1372,14 @@ function fetchReleasesViaGh(owner, repo) {
910
1372
  "ignore"
911
1373
  ]
912
1374
  });
1375
+ if (!json) return [];
913
1376
  return JSON.parse(json);
914
1377
  } catch {
915
1378
  return [];
916
1379
  }
917
1380
  }
918
1381
  async function fetchReleasesViaUngh(owner, repo) {
919
- const res = await fetch(`https://ungh.cc/repos/${owner}/${repo}/releases`, {
920
- headers: { "User-Agent": "skilld/1.0" },
921
- signal: AbortSignal.timeout(15e3)
922
- }).catch(() => null);
923
- if (!res?.ok) return [];
924
- return (await res.json().catch(() => null))?.releases ?? [];
1382
+ return (await $fetch(`https://ungh.cc/repos/${owner}/${repo}/releases`, { signal: AbortSignal.timeout(15e3) }).catch(() => null))?.releases ?? [];
925
1383
  }
926
1384
  async function fetchAllReleases(owner, repo) {
927
1385
  if (isGhAvailable()) {
@@ -947,9 +1405,48 @@ function selectReleases(releases, packageName) {
947
1405
  return compareSemver(parseSemver(verB), parseSemver(verA));
948
1406
  }).slice(0, 20);
949
1407
  }
950
- function formatRelease(release) {
1408
+ function formatRelease(release, packageName) {
951
1409
  const date = (release.publishedAt || release.createdAt).split("T")[0];
952
- return `# ${release.name || release.tag}\n\nTag: ${release.tag} | Published: ${date}\n\n${release.markdown}`;
1410
+ const version = extractVersion(release.tag, packageName) || release.tag;
1411
+ const fm = [
1412
+ "---",
1413
+ `tag: ${release.tag}`,
1414
+ `version: ${version}`,
1415
+ `published: ${date}`
1416
+ ];
1417
+ if (release.name && release.name !== release.tag) fm.push(`name: "${release.name.replace(/"/g, "\\\"")}"`);
1418
+ fm.push("---");
1419
+ return `${fm.join("\n")}\n\n# ${release.name || release.tag}\n\n${release.markdown}`;
1420
+ }
1421
+ function generateReleaseIndex(releases, packageName) {
1422
+ const lines = [
1423
+ [
1424
+ "---",
1425
+ `total: ${releases.length}`,
1426
+ `latest: ${releases[0]?.tag || "unknown"}`,
1427
+ "---"
1428
+ ].join("\n"),
1429
+ "",
1430
+ "# Releases Index",
1431
+ ""
1432
+ ];
1433
+ for (const r of releases) {
1434
+ const date = (r.publishedAt || r.createdAt).split("T")[0];
1435
+ const filename = r.tag.includes("@") || r.tag.startsWith("v") ? r.tag : `v${r.tag}`;
1436
+ const sv = parseSemver(extractVersion(r.tag, packageName) || r.tag);
1437
+ const label = sv?.patch === 0 && sv.minor === 0 ? " **[MAJOR]**" : sv?.patch === 0 ? " **[MINOR]**" : "";
1438
+ lines.push(`- [${r.tag}](./${filename}.md): ${r.name || r.tag} (${date})${label}`);
1439
+ }
1440
+ lines.push("");
1441
+ return lines.join("\n");
1442
+ }
1443
+ function isChangelogRedirectPattern(releases) {
1444
+ const sample = releases.slice(0, 3);
1445
+ if (sample.length === 0) return false;
1446
+ return sample.every((r) => {
1447
+ const body = (r.markdown || "").trim();
1448
+ return body.length < 500 && /changelog\.md/i.test(body);
1449
+ });
953
1450
  }
954
1451
  async function fetchChangelog(owner, repo, ref) {
955
1452
  for (const filename of [
@@ -957,23 +1454,36 @@ async function fetchChangelog(owner, repo, ref) {
957
1454
  "changelog.md",
958
1455
  "CHANGES.md"
959
1456
  ]) {
960
- const url = `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filename}`;
961
- const res = await fetch(url, {
962
- headers: { "User-Agent": "skilld/1.0" },
1457
+ const content = await $fetch(`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filename}`, {
1458
+ responseType: "text",
963
1459
  signal: AbortSignal.timeout(1e4)
964
1460
  }).catch(() => null);
965
- if (res?.ok) return res.text();
1461
+ if (content) return content;
966
1462
  }
967
1463
  return null;
968
1464
  }
969
1465
  async function fetchReleaseNotes(owner, repo, installedVersion, gitRef, packageName) {
970
1466
  const selected = selectReleases(await fetchAllReleases(owner, repo), packageName);
971
- if (selected.length > 0) return selected.map((r) => {
972
- return {
973
- path: `releases/${r.tag.includes("@") || r.tag.startsWith("v") ? r.tag : `v${r.tag}`}.md`,
974
- content: formatRelease(r)
975
- };
976
- });
1467
+ if (selected.length > 0) {
1468
+ if (isChangelogRedirectPattern(selected)) {
1469
+ const changelog = await fetchChangelog(owner, repo, gitRef || selected[0].tag);
1470
+ if (changelog) return [{
1471
+ path: "CHANGELOG.md",
1472
+ content: changelog
1473
+ }];
1474
+ }
1475
+ const docs = selected.map((r) => {
1476
+ return {
1477
+ path: `releases/${r.tag.includes("@") || r.tag.startsWith("v") ? r.tag : `v${r.tag}`}.md`,
1478
+ content: formatRelease(r, packageName)
1479
+ };
1480
+ });
1481
+ docs.push({
1482
+ path: "releases/_INDEX.md",
1483
+ content: generateReleaseIndex(selected, packageName)
1484
+ });
1485
+ return docs;
1486
+ }
977
1487
  const changelog = await fetchChangelog(owner, repo, gitRef || "main");
978
1488
  if (!changelog) return [];
979
1489
  return [{
@@ -981,6 +1491,6 @@ async function fetchReleaseNotes(owner, repo, installedVersion, gitRef, packageN
981
1491
  content: changelog
982
1492
  }];
983
1493
  }
984
- export { resolveEntryFiles as A, fetchText as C, verifyUrl as D, parseGitHubUrl as E, isGhAvailable as F, formatDiscussionsAsMarkdown as M, fetchGitHubIssues as N, DOC_OVERRIDES as O, formatIssuesAsMarkdown as P, fetchReadmeContent as S, normalizeRepoUrl as T, normalizeLlmsLinks as _, fetchPkgDist as a, fetchGitHubRepoMeta as b, readLocalDependencies as c, resolvePackageDocs as d, resolvePackageDocsWithAttempts as f, fetchLlmsUrl as g, fetchLlmsTxt as h, fetchNpmRegistryMeta as i, fetchGitHubDiscussions as j, getDocOverride as k, readLocalPackageInfo as l, extractSections as m, fetchLatestVersion as n, getInstalledSkillVersion as o, downloadLlmsDocs as p, fetchNpmPackage as r, parseVersionSpecifier as s, fetchReleaseNotes as t, resolveLocalPackageDocs as u, parseMarkdownLinks as v, isGitHubRepoUrl as w, fetchReadme as x, fetchGitDocs as y };
1494
+ export { extractBranchHint as A, formatDiscussionAsMarkdown as B, fetchGitDocs as C, isShallowGitDocs as D, fetchReadmeContent as E, verifyUrl as F, isGhAvailable as G, fetchGitHubIssues as H, DOC_OVERRIDES as I, getDocOverride as L, isGitHubRepoUrl as M, normalizeRepoUrl as N, validateGitDocsWithLlms as O, parseGitHubUrl as P, resolveEntryFiles as R, MIN_GIT_DOCS as S, fetchReadme as T, formatIssueAsMarkdown as U, generateDiscussionIndex as V, generateIssueIndex as W, extractSections as _, fetchNpmRegistryMeta as a, normalizeLlmsLinks as b, parseVersionSpecifier as c, resolveInstalledVersion as d, resolveLocalPackageDocs as f, downloadLlmsDocs as g, searchNpmPackages as h, fetchNpmPackage as i, fetchText as j, $fetch as k, readLocalDependencies as l, resolvePackageDocsWithAttempts as m, generateReleaseIndex as n, fetchPkgDist as o, resolvePackageDocs as p, fetchLatestVersion as r, getInstalledSkillVersion as s, fetchReleaseNotes as t, readLocalPackageInfo as u, fetchLlmsTxt as v, fetchGitHubRepoMeta as w, parseMarkdownLinks as x, fetchLlmsUrl as y, fetchGitHubDiscussions as z };
985
1495
 
986
1496
  //# sourceMappingURL=releases.mjs.map