skilld 1.3.0 → 1.5.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 (44) hide show
  1. package/README.md +54 -4
  2. package/dist/_chunks/agent.mjs +2 -1
  3. package/dist/_chunks/agent.mjs.map +1 -1
  4. package/dist/_chunks/assemble.mjs +1 -0
  5. package/dist/_chunks/assemble.mjs.map +1 -1
  6. package/dist/_chunks/author.mjs +478 -0
  7. package/dist/_chunks/author.mjs.map +1 -0
  8. package/dist/_chunks/cli-helpers.mjs +133 -2
  9. package/dist/_chunks/cli-helpers.mjs.map +1 -1
  10. package/dist/_chunks/detect.mjs.map +1 -1
  11. package/dist/_chunks/index2.d.mts +2 -0
  12. package/dist/_chunks/index2.d.mts.map +1 -1
  13. package/dist/_chunks/install.mjs +7 -17
  14. package/dist/_chunks/install.mjs.map +1 -1
  15. package/dist/_chunks/list.mjs +43 -4
  16. package/dist/_chunks/list.mjs.map +1 -1
  17. package/dist/_chunks/lockfile.mjs +140 -0
  18. package/dist/_chunks/lockfile.mjs.map +1 -0
  19. package/dist/_chunks/prepare.mjs +94 -0
  20. package/dist/_chunks/prepare.mjs.map +1 -0
  21. package/dist/_chunks/prompts.mjs +32 -43
  22. package/dist/_chunks/prompts.mjs.map +1 -1
  23. package/dist/_chunks/sanitize.mjs.map +1 -1
  24. package/dist/_chunks/search-interactive.mjs +1 -0
  25. package/dist/_chunks/search-interactive.mjs.map +1 -1
  26. package/dist/_chunks/search.mjs +146 -9
  27. package/dist/_chunks/search.mjs.map +1 -1
  28. package/dist/_chunks/setup.mjs +1 -1
  29. package/dist/_chunks/skills.mjs +28 -142
  30. package/dist/_chunks/skills.mjs.map +1 -1
  31. package/dist/_chunks/sources.mjs +4 -2
  32. package/dist/_chunks/sources.mjs.map +1 -1
  33. package/dist/_chunks/sync-shared.mjs +14 -0
  34. package/dist/_chunks/sync-shared2.mjs +1054 -0
  35. package/dist/_chunks/sync-shared2.mjs.map +1 -0
  36. package/dist/_chunks/sync.mjs +72 -1065
  37. package/dist/_chunks/sync.mjs.map +1 -1
  38. package/dist/_chunks/uninstall.mjs +5 -3
  39. package/dist/_chunks/uninstall.mjs.map +1 -1
  40. package/dist/agent/index.d.mts +4 -2
  41. package/dist/agent/index.d.mts.map +1 -1
  42. package/dist/cli.mjs +76 -10
  43. package/dist/cli.mjs.map +1 -1
  44. package/package.json +11 -10
@@ -0,0 +1,94 @@
1
+ import "./agent.mjs";
2
+ import "./config.mjs";
3
+ import "./sanitize.mjs";
4
+ import { _ as resolvePkgDir, a as getShippedSkills, f as linkShippedSkill } from "./cache.mjs";
5
+ import "./yaml.mjs";
6
+ import "./markdown.mjs";
7
+ import { n as getSharedSkillsDir } from "./shared.mjs";
8
+ import "./sources.mjs";
9
+ import { a as targets } from "./detect.mjs";
10
+ import { i as linkSkillToAgents } from "./prompts.mjs";
11
+ import { g as resolveAgent } from "./cli-helpers.mjs";
12
+ import { i as readLock, s as writeLock, t as mergeLocks } from "./lockfile.mjs";
13
+ import { t as getProjectState } from "./skills.mjs";
14
+ import { join } from "pathe";
15
+ import { existsSync, mkdirSync, symlinkSync } from "node:fs";
16
+ import * as p from "@clack/prompts";
17
+ import { defineCommand } from "citty";
18
+ //#region src/commands/prepare.ts
19
+ const prepareCommandDef = defineCommand({
20
+ meta: {
21
+ name: "prepare",
22
+ description: "Restore references and sync shipped skills (for package.json hooks)"
23
+ },
24
+ args: { agent: {
25
+ type: "enum",
26
+ options: Object.keys(targets),
27
+ alias: "a",
28
+ description: "Target agent"
29
+ } },
30
+ async run({ args }) {
31
+ const cwd = process.cwd();
32
+ const agent = resolveAgent(args.agent);
33
+ if (!agent || agent === "none") return;
34
+ const agentConfig = targets[agent];
35
+ const shared = getSharedSkillsDir(cwd);
36
+ const skillsDir = shared || join(cwd, agentConfig.skillsDir);
37
+ const allLocks = (shared ? [shared] : Object.values(targets).map((t) => join(cwd, t.skillsDir))).map((dir) => readLock(dir)).filter((l) => !!l && Object.keys(l.skills).length > 0);
38
+ if (allLocks.length > 0) {
39
+ const lock = mergeLocks(allLocks);
40
+ for (const [name, info] of Object.entries(lock.skills)) {
41
+ if (!info.version) continue;
42
+ if (info.source === "shipped") {
43
+ if (!existsSync(join(skillsDir, name))) {
44
+ const match = getShippedSkills(info.packageName || name, cwd, info.version).find((s) => s.skillName === name);
45
+ if (match) linkShippedSkill(skillsDir, name, match.skillDir);
46
+ }
47
+ continue;
48
+ }
49
+ restorePkgSymlink(skillsDir, name, info, cwd);
50
+ }
51
+ }
52
+ const state = await getProjectState(cwd);
53
+ let shippedCount = 0;
54
+ if (state.shipped.length > 0) {
55
+ mkdirSync(skillsDir, { recursive: true });
56
+ for (const entry of state.shipped) {
57
+ const version = state.deps.get(entry.packageName)?.replace(/^[\^~>=<]+/, "") || "0.0.0";
58
+ for (const skill of entry.skills) {
59
+ linkShippedSkill(skillsDir, skill.skillName, skill.skillDir);
60
+ writeLock(skillsDir, skill.skillName, {
61
+ packageName: entry.packageName,
62
+ version,
63
+ source: "shipped",
64
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
65
+ generator: "skilld"
66
+ });
67
+ if (shared) linkSkillToAgents(skill.skillName, shared, cwd, agent);
68
+ shippedCount++;
69
+ }
70
+ }
71
+ if (shippedCount > 0) p.log.success(`Installed ${shippedCount} shipped skill${shippedCount > 1 ? "s" : ""}`);
72
+ }
73
+ const freshState = shippedCount > 0 ? await getProjectState(cwd) : state;
74
+ if (freshState.outdated.length > 0) {
75
+ const n = freshState.outdated.length;
76
+ p.log.info(`${n} package${n > 1 ? "s" : ""} ha${n > 1 ? "ve" : "s"} new features and/or breaking changes. Run \`skilld update\` to sync.`);
77
+ }
78
+ }
79
+ });
80
+ /** Restore .skilld/pkg symlink to node_modules if broken */
81
+ function restorePkgSymlink(skillsDir, name, info, cwd) {
82
+ const refsDir = join(skillsDir, name, ".skilld");
83
+ const pkgLink = join(refsDir, "pkg");
84
+ if (!existsSync(join(skillsDir, name))) return;
85
+ if (existsSync(pkgLink)) return;
86
+ const pkgDir = resolvePkgDir(info.packageName || name, cwd, info.version);
87
+ if (!pkgDir) return;
88
+ mkdirSync(refsDir, { recursive: true });
89
+ symlinkSync(pkgDir, pkgLink);
90
+ }
91
+ //#endregion
92
+ export { prepareCommandDef };
93
+
94
+ //# sourceMappingURL=prepare.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepare.mjs","names":["agents"],"sources":["../../src/commands/prepare.ts"],"sourcesContent":["/**\n * Prepare command — lightweight hook for package.json \"prepare\" script.\n *\n * Designed to run on every `pnpm install` / `npm install`. Blocking, fast, no LLM calls.\n * 1. Restore broken symlinks from lockfile (like `install` but skips doc fetching)\n * 2. Auto-install shipped skills from deps (just symlinks + lockfile writes)\n * 3. Report outdated skills count and suggest `skilld update`\n */\n\nimport type { SkillInfo } from '../core/lockfile.ts'\nimport { existsSync, mkdirSync, symlinkSync } from 'node:fs'\nimport * as p from '@clack/prompts'\nimport { defineCommand } from 'citty'\nimport { join } from 'pathe'\nimport { agents, linkSkillToAgents } from '../agent/index.ts'\nimport { getShippedSkills, linkShippedSkill, resolvePkgDir } from '../cache/index.ts'\nimport { resolveAgent } from '../cli-helpers.ts'\nimport { mergeLocks, readLock, writeLock } from '../core/lockfile.ts'\nimport { getSharedSkillsDir } from '../core/shared.ts'\nimport { getProjectState } from '../core/skills.ts'\n\nexport const prepareCommandDef = defineCommand({\n meta: { name: 'prepare', description: 'Restore references and sync shipped skills (for package.json hooks)' },\n args: {\n agent: {\n type: 'enum' as const,\n options: Object.keys(agents),\n alias: 'a',\n description: 'Target agent',\n },\n },\n async run({ args }) {\n const cwd = process.cwd()\n\n const agent = resolveAgent(args.agent)\n if (!agent || agent === 'none')\n return\n\n const agentConfig = agents[agent]\n const shared = getSharedSkillsDir(cwd)\n const skillsDir = shared || join(cwd, agentConfig.skillsDir)\n\n // ── 1. Restore broken symlinks from lockfile ──\n\n const allSkillsDirs = shared\n ? [shared]\n : Object.values(agents).map(t => join(cwd, t.skillsDir))\n const allLocks = allSkillsDirs\n .map(dir => readLock(dir))\n .filter((l): l is NonNullable<typeof l> => !!l && Object.keys(l.skills).length > 0)\n\n if (allLocks.length > 0) {\n const lock = mergeLocks(allLocks)\n\n for (const [name, info] of Object.entries(lock.skills)) {\n if (!info.version)\n continue\n\n if (info.source === 'shipped') {\n const skillDir = join(skillsDir, name)\n if (!existsSync(skillDir)) {\n const pkgName = info.packageName || name\n const shipped = getShippedSkills(pkgName, cwd, info.version)\n const match = shipped.find(s => s.skillName === name)\n if (match)\n linkShippedSkill(skillsDir, name, match.skillDir)\n }\n continue\n }\n\n // Non-shipped: restore .skilld/pkg symlink if broken\n restorePkgSymlink(skillsDir, name, info, cwd)\n }\n }\n\n // ── 2. Auto-install shipped skills from deps ──\n\n const state = await getProjectState(cwd)\n let shippedCount = 0\n\n if (state.shipped.length > 0) {\n mkdirSync(skillsDir, { recursive: true })\n\n for (const entry of state.shipped) {\n const version = state.deps.get(entry.packageName)?.replace(/^[\\^~>=<]+/, '') || '0.0.0'\n\n for (const skill of entry.skills) {\n linkShippedSkill(skillsDir, skill.skillName, skill.skillDir)\n writeLock(skillsDir, skill.skillName, {\n packageName: entry.packageName,\n version,\n source: 'shipped',\n syncedAt: new Date().toISOString().split('T')[0],\n generator: 'skilld',\n })\n\n if (shared)\n linkSkillToAgents(skill.skillName, shared, cwd, agent)\n\n shippedCount++\n }\n }\n\n if (shippedCount > 0)\n p.log.success(`Installed ${shippedCount} shipped skill${shippedCount > 1 ? 's' : ''}`)\n }\n\n // ── 3. Report outdated skills ──\n\n // Re-read state after shipped installs so they don't show as missing\n const freshState = shippedCount > 0 ? await getProjectState(cwd) : state\n\n if (freshState.outdated.length > 0) {\n const n = freshState.outdated.length\n p.log.info(`${n} package${n > 1 ? 's' : ''} ha${n > 1 ? 've' : 's'} new features and/or breaking changes. Run \\`skilld update\\` to sync.`)\n }\n },\n})\n\n/** Restore .skilld/pkg symlink to node_modules if broken */\nfunction restorePkgSymlink(skillsDir: string, name: string, info: SkillInfo, cwd: string): void {\n const refsDir = join(skillsDir, name, '.skilld')\n const pkgLink = join(refsDir, 'pkg')\n\n // Only fix if the skill dir exists but the pkg symlink is broken\n if (!existsSync(join(skillsDir, name)))\n return\n\n if (existsSync(pkgLink))\n return\n\n const pkgName = info.packageName || name\n const pkgDir = resolvePkgDir(pkgName, cwd, info.version)\n if (!pkgDir)\n return\n\n mkdirSync(refsDir, { recursive: true })\n symlinkSync(pkgDir, pkgLink)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBA,MAAa,oBAAoB,cAAc;CAC7C,MAAM;EAAE,MAAM;EAAW,aAAa;EAAuE;CAC7G,MAAM,EACJ,OAAO;EACL,MAAM;EACN,SAAS,OAAO,KAAKA,QAAO;EAC5B,OAAO;EACP,aAAa;EACd,EACF;CACD,MAAM,IAAI,EAAE,QAAQ;EAClB,MAAM,MAAM,QAAQ,KAAK;EAEzB,MAAM,QAAQ,aAAa,KAAK,MAAM;AACtC,MAAI,CAAC,SAAS,UAAU,OACtB;EAEF,MAAM,cAAcA,QAAO;EAC3B,MAAM,SAAS,mBAAmB,IAAI;EACtC,MAAM,YAAY,UAAU,KAAK,KAAK,YAAY,UAAU;EAO5D,MAAM,YAHgB,SAClB,CAAC,OAAO,GACR,OAAO,OAAOA,QAAO,CAAC,KAAI,MAAK,KAAK,KAAK,EAAE,UAAU,CAAC,EAEvD,KAAI,QAAO,SAAS,IAAI,CAAC,CACzB,QAAQ,MAAkC,CAAC,CAAC,KAAK,OAAO,KAAK,EAAE,OAAO,CAAC,SAAS,EAAE;AAErF,MAAI,SAAS,SAAS,GAAG;GACvB,MAAM,OAAO,WAAW,SAAS;AAEjC,QAAK,MAAM,CAAC,MAAM,SAAS,OAAO,QAAQ,KAAK,OAAO,EAAE;AACtD,QAAI,CAAC,KAAK,QACR;AAEF,QAAI,KAAK,WAAW,WAAW;AAE7B,SAAI,CAAC,WADY,KAAK,WAAW,KAAK,CACb,EAAE;MAGzB,MAAM,QADU,iBADA,KAAK,eAAe,MACM,KAAK,KAAK,QAAQ,CACtC,MAAK,MAAK,EAAE,cAAc,KAAK;AACrD,UAAI,MACF,kBAAiB,WAAW,MAAM,MAAM,SAAS;;AAErD;;AAIF,sBAAkB,WAAW,MAAM,MAAM,IAAI;;;EAMjD,MAAM,QAAQ,MAAM,gBAAgB,IAAI;EACxC,IAAI,eAAe;AAEnB,MAAI,MAAM,QAAQ,SAAS,GAAG;AAC5B,aAAU,WAAW,EAAE,WAAW,MAAM,CAAC;AAEzC,QAAK,MAAM,SAAS,MAAM,SAAS;IACjC,MAAM,UAAU,MAAM,KAAK,IAAI,MAAM,YAAY,EAAE,QAAQ,cAAc,GAAG,IAAI;AAEhF,SAAK,MAAM,SAAS,MAAM,QAAQ;AAChC,sBAAiB,WAAW,MAAM,WAAW,MAAM,SAAS;AAC5D,eAAU,WAAW,MAAM,WAAW;MACpC,aAAa,MAAM;MACnB;MACA,QAAQ;MACR,2BAAU,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,IAAI,CAAC;MAC9C,WAAW;MACZ,CAAC;AAEF,SAAI,OACF,mBAAkB,MAAM,WAAW,QAAQ,KAAK,MAAM;AAExD;;;AAIJ,OAAI,eAAe,EACjB,GAAE,IAAI,QAAQ,aAAa,aAAa,gBAAgB,eAAe,IAAI,MAAM,KAAK;;EAM1F,MAAM,aAAa,eAAe,IAAI,MAAM,gBAAgB,IAAI,GAAG;AAEnE,MAAI,WAAW,SAAS,SAAS,GAAG;GAClC,MAAM,IAAI,WAAW,SAAS;AAC9B,KAAE,IAAI,KAAK,GAAG,EAAE,UAAU,IAAI,IAAI,MAAM,GAAG,KAAK,IAAI,IAAI,OAAO,IAAI,uEAAuE;;;CAG/I,CAAC;;AAGF,SAAS,kBAAkB,WAAmB,MAAc,MAAiB,KAAmB;CAC9F,MAAM,UAAU,KAAK,WAAW,MAAM,UAAU;CAChD,MAAM,UAAU,KAAK,SAAS,MAAM;AAGpC,KAAI,CAAC,WAAW,KAAK,WAAW,KAAK,CAAC,CACpC;AAEF,KAAI,WAAW,QAAQ,CACrB;CAGF,MAAM,SAAS,cADC,KAAK,eAAe,MACE,KAAK,KAAK,QAAQ;AACxD,KAAI,CAAC,OACH;AAEF,WAAU,SAAS,EAAE,WAAW,MAAM,CAAC;AACvC,aAAY,QAAQ,QAAQ"}
@@ -8,14 +8,23 @@ import { existsSync, lstatSync, mkdirSync, symlinkSync, unlinkSync, writeFileSyn
8
8
  /**
9
9
  * Dynamic budget allocation for skill sections.
10
10
  *
11
- * Total SKILL.md body should stay under ~300 lines (≈5,000 words per Agent Skills guide).
12
- * When more sections are enabled, each gets proportionally less space.
13
- * When a package has many releases, API changes budget scales up to capture more churn.
11
+ * Total SKILL.md target is ~500 lines. Overhead (frontmatter, header, search, footer)
12
+ * is subtracted to get the available body budget, which is divided among enabled sections.
13
+ * When a package has many releases, budgets scale up.
14
14
  */
15
- /** Scale max lines based on enabled section count. Solo sections get full budget, 4 sections ~60%. */
16
- function maxLines(min, max, sectionCount) {
15
+ const TOTAL_TARGET = 500;
16
+ const DEFAULT_OVERHEAD = 30;
17
+ /** Available body lines after overhead is subtracted */
18
+ function remainingLines(overheadLines) {
19
+ return TOTAL_TARGET - (overheadLines ?? DEFAULT_OVERHEAD);
20
+ }
21
+ /** Scale max lines based on enabled section count and available remaining space. */
22
+ function maxLines(min, max, sectionCount, overheadLines) {
23
+ const remaining = remainingLines(overheadLines);
24
+ const sections = Math.max(1, sectionCount ?? 1);
25
+ const perSection = Math.floor(remaining / sections);
17
26
  const scale = budgetScale(sectionCount);
18
- return Math.max(min, Math.round(max * scale));
27
+ return Math.max(min, Math.min(Math.round(max * scale), perSection));
19
28
  }
20
29
  /** Scale item count based on enabled section count. */
21
30
  function maxItems(min, max, sectionCount) {
@@ -76,7 +85,7 @@ function checkAbsolutePaths(content) {
76
85
  }
77
86
  //#endregion
78
87
  //#region src/agent/prompts/optional/api-changes.ts
79
- function apiChangesSection({ packageName, version, hasReleases, hasChangelog, hasDocs, hasIssues, hasDiscussions, pkgFiles, features, enabledSectionCount, releaseCount }) {
88
+ function apiChangesSection({ packageName, version, hasReleases, hasChangelog, hasDocs, hasIssues, hasDiscussions, pkgFiles, features, enabledSectionCount, releaseCount, overheadLines }) {
80
89
  const [, major, minor] = version?.match(/^(\d+)\.(\d+)/) ?? [];
81
90
  const boost = releaseBoost(releaseCount, minor ? Number(minor) : void 0);
82
91
  const cmd = resolveSkilldCommand();
@@ -141,7 +150,7 @@ function apiChangesSection({ packageName, version, hasReleases, hasChangelog, ha
141
150
  | Renamed/moved | 3 | 1 | 0 |
142
151
 
143
152
  The "Older" column means ≤ v${Number(major) - 2}.x — these changes are NOT useful because anyone on v${major}.x already migrated past them.` : "";
144
- const apiChangesMaxLines = maxLines(50, Math.round(80 * boost), enabledSectionCount);
153
+ const apiChangesMaxLines = maxLines(60, Math.round(130 * boost), enabledSectionCount, overheadLines);
145
154
  return {
146
155
  referenceWeights,
147
156
  validate(content) {
@@ -185,7 +194,7 @@ Each item: BREAKING/DEPRECATED/NEW label + API name + what changed + source link
185
194
 
186
195
  **Tiered format:** Top-scoring items get full detailed entries. Remaining relevant items go in a compact "**Also changed:**" line at the end — API name + brief label, separated by \` · \`. This surfaces more changes without bloating the section.`,
187
196
  rules: [
188
- `- **API Changes:** ${maxItems(6, Math.round(12 * boost), enabledSectionCount)} detailed items + compact "Also changed" line for remaining, MAX ${apiChangesMaxLines} lines`,
197
+ `- **API Changes:** ${maxItems(8, Math.round(18 * boost), enabledSectionCount)} detailed items + compact "Also changed" line for remaining, MAX ${apiChangesMaxLines} lines`,
189
198
  "- **Every detailed item MUST have a `[source](./.skilld/...#section)` link** with a section anchor (`#heading-slug`) or line reference (`:L<line>` or `:L<start>:<end>`). If you cannot cite a specific location in a release, changelog entry, or migration doc, do NOT include the item",
190
199
  "- **Recency:** Only include changes from the current major version and the previous→current migration. Exclude changes from older major versions entirely — users already migrated past them",
191
200
  "- Focus on APIs that CHANGED, not general conventions or gotchas",
@@ -200,7 +209,7 @@ Each item: BREAKING/DEPRECATED/NEW label + API name + what changed + source link
200
209
  }
201
210
  //#endregion
202
211
  //#region src/agent/prompts/optional/best-practices.ts
203
- function bestPracticesSection({ packageName, hasIssues, hasDiscussions, hasReleases, hasChangelog, hasDocs, pkgFiles, features, enabledSectionCount, releaseCount, version }) {
212
+ function bestPracticesSection({ packageName, hasIssues, hasDiscussions, hasReleases, hasChangelog, hasDocs, pkgFiles, features, enabledSectionCount, releaseCount, version, overheadLines }) {
204
213
  const [, , minor] = version?.match(/^(\d+)\.(\d+)/) ?? [];
205
214
  const boost = 1 + (releaseBoost(releaseCount, minor ? Number(minor) : void 0) - 1) * .5;
206
215
  const cmd = resolveSkilldCommand();
@@ -237,7 +246,7 @@ function bestPracticesSection({ packageName, hasIssues, hasDiscussions, hasRelea
237
246
  score: 3,
238
247
  useFor: "Only for new patterns introduced in recent versions"
239
248
  });
240
- const bpMaxLines = maxLines(80, Math.round(150 * boost), enabledSectionCount);
249
+ const bpMaxLines = maxLines(100, Math.round(250 * boost), enabledSectionCount, overheadLines);
241
250
  return {
242
251
  referenceWeights,
243
252
  validate(content) {
@@ -279,7 +288,7 @@ const client = createX({ retryDelay: attempt => Math.min(1000 * 2 ** attempt, 30
279
288
 
280
289
  Each item: markdown list item (-) + ${packageName}-specific pattern + why it's preferred + \`[source](./.skilld/...#section)\` link. **Prefer concise descriptions over inline code** — the source link points the agent to full examples in the docs. Only add a code block when the pattern genuinely cannot be understood from the description alone (e.g., non-obvious syntax, multi-step wiring). Most items should be description + source link only. All source links MUST use \`./.skilld/\` prefix and include a **section anchor** (\`#heading-slug\`) or **line reference** (\`:L<line>\` or \`:L<start>:<end>\`) to pinpoint the exact location. Do NOT use emoji — use plain text markers only.`,
281
290
  rules: [
282
- `- **${maxItems(4, Math.round(10 * boost), enabledSectionCount)} best practice items**`,
291
+ `- **${maxItems(6, Math.round(15 * boost), enabledSectionCount)} best practice items**`,
283
292
  `- **MAX ${bpMaxLines} lines** for best practices section`,
284
293
  "- **Every item MUST have a `[source](./.skilld/...#section)` link** with a section anchor (`#heading-slug`) or line reference (`:L<line>` or `:L<start>:<end>`). If you cannot cite a specific location in a reference file, do NOT include the item — unsourced items risk hallucination and will be rejected",
285
294
  "- **Minimize inline code.** Most items should be description + source link only. The source file contains full examples the agent can read. Only add a code block when the pattern is unintuitable from the description (non-obvious syntax, surprising argument order, multi-step wiring). Aim for at most 1 in 4 items having a code block",
@@ -293,8 +302,8 @@ Each item: markdown list item (-) + ${packageName}-specific pattern + why it's p
293
302
  }
294
303
  //#endregion
295
304
  //#region src/agent/prompts/optional/custom.ts
296
- function customSection({ heading, body }, enabledSectionCount) {
297
- const customMaxLines = maxLines(50, 80, enabledSectionCount);
305
+ function customSection({ heading, body }, enabledSectionCount, overheadLines) {
306
+ const customMaxLines = maxLines(50, 80, enabledSectionCount, overheadLines);
298
307
  return {
299
308
  validate(content) {
300
309
  return [
@@ -375,16 +384,9 @@ function generateImportantBlock({ packageName, hasIssues, hasDiscussions, hasRel
375
384
  ...rows.map(([desc, cmd]) => `| ${desc} | ${cmd} |`)
376
385
  ].join("\n");
377
386
  const cmd = resolveSkilldCommand();
378
- const fallbackCmd = cmd === "skilld" ? "npx -y skilld" : "skilld";
379
387
  return `**IMPORTANT:** Use these references${features?.search !== false ? `\n\n## Search
380
388
 
381
- Use \`${cmd} search\` as your primary research tool — search before manually reading files. If \`${cmd}\` is unavailable, use \`${fallbackCmd} search\`.
382
-
383
- \`\`\`bash
384
- ${cmd} search "<query>" -p ${packageName}
385
- ${hasIssues ? `${cmd} search "issues:<query>" -p ${packageName}\n` : ""}${hasReleases ? `${cmd} search "releases:<query>" -p ${packageName}\n` : ""}\`\`\`
386
-
387
- Filters: \`docs:\`, \`issues:\`, \`releases:\` prefix narrows by source type.` : ""}
389
+ Use \`${cmd} search "query" -p ${packageName}\` as your primary research tool — search before manually reading files. Run \`${cmd} search --guide -p ${packageName}\` for full syntax.` : ""}
388
390
 
389
391
  ${table}`;
390
392
  }
@@ -419,7 +421,7 @@ function getSectionDef(section, ctx, customPrompt) {
419
421
  switch (section) {
420
422
  case "api-changes": return apiChangesSection(ctx);
421
423
  case "best-practices": return bestPracticesSection(ctx);
422
- case "custom": return customPrompt ? customSection(customPrompt, ctx.enabledSectionCount) : null;
424
+ case "custom": return customPrompt ? customSection(customPrompt, ctx.enabledSectionCount, ctx.overheadLines) : null;
423
425
  }
424
426
  }
425
427
  /**
@@ -459,7 +461,8 @@ function buildSectionPrompt(opts) {
459
461
  pkgFiles: opts.pkgFiles,
460
462
  features: opts.features,
461
463
  enabledSectionCount: opts.enabledSectionCount,
462
- releaseCount
464
+ releaseCount,
465
+ overheadLines: opts.overheadLines
463
466
  }, customPrompt);
464
467
  if (!sectionDef) return "";
465
468
  const outputFile = SECTION_OUTPUT_FILES[section];
@@ -674,7 +677,7 @@ function unlinkSkillFromAgents(skillName, cwd, agentType) {
674
677
  //#region src/agent/prompts/skill.ts
675
678
  function generateSkillMd(opts) {
676
679
  const header = generatePackageHeader(opts);
677
- const search = !opts.eject && opts.features?.search !== false ? generateSearchBlock(opts.name, opts.hasIssues, opts.hasReleases) : "";
680
+ const search = !opts.eject && opts.features?.search !== false ? generateSearchBlock(opts.name) : "";
678
681
  let body = opts.body;
679
682
  if (body && opts.eject) {
680
683
  body = body.replace(/\.\/\.skilld\//g, "./references/");
@@ -703,7 +706,7 @@ function formatShortDate(isoDate) {
703
706
  "Dec"
704
707
  ][date.getUTCMonth()]} ${date.getUTCFullYear()}`;
705
708
  }
706
- function generatePackageHeader({ name, description, version, releasedAt, dependencies, distTags, repoUrl, hasIssues, hasDiscussions, hasReleases, docsType, pkgFiles, packages, eject }) {
709
+ function generatePackageHeader({ name, description, version, releasedAt, distTags, repoUrl, hasIssues, hasDiscussions, hasReleases, docsType, pkgFiles, packages, eject }) {
707
710
  let title = `# ${name}`;
708
711
  if (repoUrl) {
709
712
  const url = repoUrl.startsWith("http") ? repoUrl : `https://github.com/${repoUrl}`;
@@ -716,12 +719,8 @@ function generatePackageHeader({ name, description, version, releasedAt, depende
716
719
  const versionStr = dateStr ? `${version} (${dateStr})` : version;
717
720
  lines.push("", `**Version:** ${versionStr}`);
718
721
  }
719
- if (dependencies && Object.keys(dependencies).length > 0) {
720
- const deps = Object.entries(dependencies).map(([n, v]) => `${n}@${v}`).join(", ");
721
- lines.push(`**Deps:** ${deps}`);
722
- }
723
722
  if (distTags && Object.keys(distTags).length > 0) {
724
- const tags = Object.entries(distTags).map(([tag, info]) => {
723
+ const tags = Object.entries(distTags).sort(([, a], [, b]) => (b.releasedAt ?? "").localeCompare(a.releasedAt ?? "")).slice(0, 3).map(([tag, info]) => {
725
724
  const relDate = info.releasedAt ? ` (${formatShortDate(info.releasedAt)})` : "";
726
725
  return `${tag}: ${info.version}${relDate}`;
727
726
  }).join(", ");
@@ -816,21 +815,11 @@ function generateFrontmatter({ name, version, description: pkgDescription, globs
816
815
  lines.push("---", "", "");
817
816
  return lines.join("\n");
818
817
  }
819
- function generateSearchBlock(name, hasIssues, hasReleases) {
818
+ function generateSearchBlock(name) {
820
819
  const cmd = resolveSkilldCommand();
821
- const fallbackCmd = cmd === "skilld" ? "npx -y skilld" : "skilld";
822
- const examples = [`${cmd} search "query" -p ${name}`];
823
- if (hasIssues) examples.push(`${cmd} search "issues:error handling" -p ${name}`);
824
- if (hasReleases) examples.push(`${cmd} search "releases:deprecated" -p ${name}`);
825
820
  return `## Search
826
821
 
827
- Use \`${cmd} search\` instead of grepping \`.skilld/\` directories hybrid semantic + keyword search across all indexed docs, issues, and releases. If \`${cmd}\` is unavailable, use \`${fallbackCmd} search\`.
828
-
829
- \`\`\`bash
830
- ${examples.join("\n")}
831
- \`\`\`
832
-
833
- Filters: \`docs:\`, \`issues:\`, \`releases:\` prefix narrows by source type.`;
822
+ Use \`${cmd} search "query" -p ${name}\` instead of grepping \`.skilld/\` directories. Run \`${cmd} search --guide -p ${name}\` for full syntax, filters, and operators.`;
834
823
  }
835
824
  function generateFooter(relatedSkills) {
836
825
  if (relatedSkills.length === 0) return "";