@skillit/core 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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/dist/audit-formatter.d.ts +36 -0
  4. package/dist/audit-formatter.d.ts.map +1 -0
  5. package/dist/audit-formatter.js +173 -0
  6. package/dist/audit-formatter.js.map +1 -0
  7. package/dist/audit-score.d.ts +76 -0
  8. package/dist/audit-score.d.ts.map +1 -0
  9. package/dist/audit-score.js +490 -0
  10. package/dist/audit-score.js.map +1 -0
  11. package/dist/audit-types.d.ts +81 -0
  12. package/dist/audit-types.d.ts.map +1 -0
  13. package/dist/audit-types.js +2 -0
  14. package/dist/audit-types.js.map +1 -0
  15. package/dist/audit.d.ts +17 -0
  16. package/dist/audit.d.ts.map +1 -0
  17. package/dist/audit.js +567 -0
  18. package/dist/audit.js.map +1 -0
  19. package/dist/canonical.d.ts +36 -0
  20. package/dist/canonical.d.ts.map +1 -0
  21. package/dist/canonical.js +148 -0
  22. package/dist/canonical.js.map +1 -0
  23. package/dist/config-renderer.d.ts +16 -0
  24. package/dist/config-renderer.d.ts.map +1 -0
  25. package/dist/config-renderer.js +296 -0
  26. package/dist/config-renderer.js.map +1 -0
  27. package/dist/config-types.d.ts +144 -0
  28. package/dist/config-types.d.ts.map +1 -0
  29. package/dist/config-types.js +2 -0
  30. package/dist/config-types.js.map +1 -0
  31. package/dist/docs-scanner.d.ts +40 -0
  32. package/dist/docs-scanner.d.ts.map +1 -0
  33. package/dist/docs-scanner.js +176 -0
  34. package/dist/docs-scanner.js.map +1 -0
  35. package/dist/examples-scanner.d.ts +39 -0
  36. package/dist/examples-scanner.d.ts.map +1 -0
  37. package/dist/examples-scanner.js +221 -0
  38. package/dist/examples-scanner.js.map +1 -0
  39. package/dist/index.d.ts +40 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +31 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/llms-txt.d.ts +20 -0
  44. package/dist/llms-txt.d.ts.map +1 -0
  45. package/dist/llms-txt.js +187 -0
  46. package/dist/llms-txt.js.map +1 -0
  47. package/dist/markdown-parser.d.ts +11 -0
  48. package/dist/markdown-parser.d.ts.map +1 -0
  49. package/dist/markdown-parser.js +222 -0
  50. package/dist/markdown-parser.js.map +1 -0
  51. package/dist/markdown-types.d.ts +48 -0
  52. package/dist/markdown-types.d.ts.map +1 -0
  53. package/dist/markdown-types.js +2 -0
  54. package/dist/markdown-types.js.map +1 -0
  55. package/dist/readme-parser.d.ts +21 -0
  56. package/dist/readme-parser.d.ts.map +1 -0
  57. package/dist/readme-parser.js +137 -0
  58. package/dist/readme-parser.js.map +1 -0
  59. package/dist/references-mcp.d.ts +79 -0
  60. package/dist/references-mcp.d.ts.map +1 -0
  61. package/dist/references-mcp.js +130 -0
  62. package/dist/references-mcp.js.map +1 -0
  63. package/dist/refine/ast-edit.d.ts +21 -0
  64. package/dist/refine/ast-edit.d.ts.map +1 -0
  65. package/dist/refine/ast-edit.js +147 -0
  66. package/dist/refine/ast-edit.js.map +1 -0
  67. package/dist/refine/index.d.ts +7 -0
  68. package/dist/refine/index.d.ts.map +1 -0
  69. package/dist/refine/index.js +6 -0
  70. package/dist/refine/index.js.map +1 -0
  71. package/dist/refine/jsdoc-edit.d.ts +4 -0
  72. package/dist/refine/jsdoc-edit.d.ts.map +1 -0
  73. package/dist/refine/jsdoc-edit.js +6 -0
  74. package/dist/refine/jsdoc-edit.js.map +1 -0
  75. package/dist/refine/loop.d.ts +8 -0
  76. package/dist/refine/loop.d.ts.map +1 -0
  77. package/dist/refine/loop.js +98 -0
  78. package/dist/refine/loop.js.map +1 -0
  79. package/dist/refine/select-targets.d.ts +5 -0
  80. package/dist/refine/select-targets.d.ts.map +1 -0
  81. package/dist/refine/select-targets.js +30 -0
  82. package/dist/refine/select-targets.js.map +1 -0
  83. package/dist/refine/types.d.ts +67 -0
  84. package/dist/refine/types.d.ts.map +1 -0
  85. package/dist/refine/types.js +2 -0
  86. package/dist/refine/types.js.map +1 -0
  87. package/dist/renderer.d.ts +27 -0
  88. package/dist/renderer.d.ts.map +1 -0
  89. package/dist/renderer.js +1244 -0
  90. package/dist/renderer.js.map +1 -0
  91. package/dist/tokens.d.ts +14 -0
  92. package/dist/tokens.d.ts.map +1 -0
  93. package/dist/tokens.js +26 -0
  94. package/dist/tokens.js.map +1 -0
  95. package/dist/types.d.ts +519 -0
  96. package/dist/types.d.ts.map +1 -0
  97. package/dist/types.js +2 -0
  98. package/dist/types.js.map +1 -0
  99. package/dist/writer.d.ts +17 -0
  100. package/dist/writer.d.ts.map +1 -0
  101. package/dist/writer.js +313 -0
  102. package/dist/writer.js.map +1 -0
  103. package/package.json +54 -0
@@ -0,0 +1,1244 @@
1
+ import YAML from 'yaml';
2
+ import { estimateTokens, truncateToTokenBudget } from './tokens.js';
3
+ import { renderConfigSurfaceSection, renderConfigReference } from './config-renderer.js';
4
+ import { canonicalize } from './canonical.js';
5
+ import { renderResourcesReference, renderPromptsReference } from './references-mcp.js';
6
+ /** agentskills.io spec: max 1024 chars for description */
7
+ const DESCRIPTION_MAX = 1024;
8
+ const DEFAULT_OPTIONS = {
9
+ outDir: 'skills',
10
+ includeExamples: true,
11
+ includeSignatures: true,
12
+ maxTokens: 4000,
13
+ namePrefix: '',
14
+ license: ''
15
+ };
16
+ const REF_CATEGORY_ORDER = [
17
+ 'functions',
18
+ 'classes',
19
+ 'types',
20
+ 'variables',
21
+ 'commands',
22
+ 'config',
23
+ 'docs',
24
+ 'resources',
25
+ 'prompts',
26
+ 'examples'
27
+ ];
28
+ /**
29
+ * Render multiple extracted skills into progressive disclosure file sets.
30
+ *
31
+ * @category Rendering
32
+ * @useWhen
33
+ * - You have one or more ExtractedSkill objects and need SKILL.md + references/ output
34
+ * - Building a custom extraction pipeline that bypasses the TypeDoc plugin
35
+ * @never
36
+ * - NEVER set maxTokens below 500 — reference files become truncated mid-signature, producing broken code blocks that confuse LLMs
37
+ * - NEVER pass skills with empty `name` — the output directory becomes a bare `/` path
38
+ */
39
+ export function renderSkills(skills, options) {
40
+ const opts = { ...DEFAULT_OPTIONS, ...options };
41
+ const rendered = skills.map((skill) => renderSkill(skill, opts));
42
+ // Generate a router skill when multiple skills exist (monorepo)
43
+ if (skills.length > 1) {
44
+ const router = renderRouterSkill(skills, opts);
45
+ if (router)
46
+ rendered.push(router);
47
+ }
48
+ return rendered;
49
+ }
50
+ export function renderSkill(skill, options) {
51
+ const opts = { ...DEFAULT_OPTIONS, ...options };
52
+ // --- Invocation adapter path: delegate dialect-specific rendering. ---
53
+ // The host still canonicalizes the adapter's output so re-runs are content-identical.
54
+ if (opts.invocation) {
55
+ // Determine `mode` deterministically from SkillRenderOptions. Exactly one
56
+ // of invocationPackageName | invocationHttpEndpoint | invocationLaunchCommand
57
+ // must be set — the DU encodes this as a compile-time invariant on
58
+ // `AdapterRenderContext` and we surface a runtime error here for any
59
+ // non-literal callers (programmatic API consumers).
60
+ const hasPackage = opts.invocationPackageName !== undefined;
61
+ const hasHttp = opts.invocationHttpEndpoint !== undefined;
62
+ const hasStdio = opts.invocationLaunchCommand !== undefined;
63
+ const setCount = (hasPackage ? 1 : 0) + (hasHttp ? 1 : 0) + (hasStdio ? 1 : 0);
64
+ if (setCount > 1) {
65
+ // NOTE: McpError lives in @skillit/mcp and core has no dependency on
66
+ // mcp. Plain Error is fine — the mcp wrapper that invokes renderSkill
67
+ // (bundle.ts → recordFailure) maps non-McpError throws onto
68
+ // TRANSPORT_FAILED so the CLI exit-code mapper still sees a stable code.
69
+ throw new Error('AdapterRenderContext: more than one of invocationPackageName, invocationHttpEndpoint, invocationLaunchCommand was set; the renderer requires exactly one to determine the launch shape (FR-H002).');
70
+ }
71
+ if (setCount === 0) {
72
+ throw new Error('AdapterRenderContext: missing launch info — set one of invocationPackageName, invocationHttpEndpoint, invocationLaunchCommand on SkillRenderOptions when invoking renderSkill with an invocation adapter (FR-H002).');
73
+ }
74
+ const skillName = toSkillName(opts.namePrefix || skill.name);
75
+ let ctx;
76
+ if (hasPackage) {
77
+ ctx = {
78
+ mode: 'bundle',
79
+ skillName,
80
+ maxTokens: opts.maxTokens,
81
+ canonicalize: true,
82
+ packageName: opts.invocationPackageName,
83
+ ...(opts.invocationBinName !== undefined ? { binName: opts.invocationBinName } : {})
84
+ };
85
+ }
86
+ else if (hasHttp) {
87
+ ctx = {
88
+ mode: 'http',
89
+ skillName,
90
+ maxTokens: opts.maxTokens,
91
+ canonicalize: true,
92
+ httpEndpoint: opts.invocationHttpEndpoint
93
+ };
94
+ }
95
+ else {
96
+ // hasStdio
97
+ ctx = {
98
+ mode: 'stdio',
99
+ skillName,
100
+ maxTokens: opts.maxTokens,
101
+ canonicalize: true,
102
+ launchCommand: opts.invocationLaunchCommand
103
+ };
104
+ }
105
+ return Promise.resolve(opts.invocation.render(skill, ctx)).then((rendered) => canonicalize(rendered));
106
+ }
107
+ // --- Default path: preserve today's synchronous output shape exactly. ---
108
+ const skillName = toSkillName(opts.namePrefix || skill.name);
109
+ const basePath = skillName;
110
+ // --- references/*.md: detailed API loaded on demand ---
111
+ const references = [];
112
+ if (skill.functions.length > 0 && !opts.skipDefaultFunctionsRef) {
113
+ addGroupedReferences(skill.functions, basePath, 'functions', opts, (items) => renderFunctionsRef(items, opts), 'Function', references);
114
+ }
115
+ if (skill.classes.length > 0) {
116
+ addGroupedReferences(skill.classes, basePath, 'classes', opts, (items) => renderClassesRef(items, opts), 'Class', references);
117
+ }
118
+ if (skill.types.length > 0 || skill.enums.length > 0) {
119
+ const content = renderTypesRef(skill.types, skill.enums);
120
+ references.push({
121
+ filename: `${basePath}/references/types.md`,
122
+ content: truncateToTokenBudget(content, opts.maxTokens),
123
+ tokens: estimateTokens(content)
124
+ });
125
+ }
126
+ if (skill.variables && skill.variables.length > 0) {
127
+ const content = renderVariablesRef(skill.variables);
128
+ references.push({
129
+ filename: `${basePath}/references/variables.md`,
130
+ content: truncateToTokenBudget(content, opts.maxTokens),
131
+ tokens: estimateTokens(content)
132
+ });
133
+ }
134
+ // Only create examples.md for examples beyond the first (which is used as Quick Start in SKILL.md)
135
+ if (opts.includeExamples && skill.examples.length > 1) {
136
+ const content = '# Examples\n\n' + skill.examples.slice(1).join('\n\n---\n\n');
137
+ references.push({
138
+ filename: `${basePath}/references/examples.md`,
139
+ content: truncateToTokenBudget(content, opts.maxTokens),
140
+ tokens: estimateTokens(content)
141
+ });
142
+ }
143
+ if (skill.documents && skill.documents.length > 0) {
144
+ // Group docs by category for index files
145
+ const byCategory = new Map();
146
+ for (const doc of skill.documents) {
147
+ const cat = doc.category ?? '_uncategorized';
148
+ const list = byCategory.get(cat) ?? [];
149
+ list.push(doc);
150
+ byCategory.set(cat, list);
151
+ }
152
+ // Generate per-category index files
153
+ for (const [cat, docs] of byCategory) {
154
+ if (cat === '_uncategorized' && docs.length < 3)
155
+ continue; // skip tiny uncategorized groups
156
+ const subdir = cat === '_uncategorized' ? 'docs' : `docs/${cat}`;
157
+ const indexLines = [`# ${cat === '_uncategorized' ? 'Documentation' : cat}\n`];
158
+ for (const doc of docs) {
159
+ const desc = doc.description ? ` — ${doc.description}` : '';
160
+ indexLines.push(`- [${doc.title}](${toFilename(doc.title)}.md)${desc}`);
161
+ }
162
+ references.push({
163
+ filename: `${basePath}/references/${subdir}/index.md`,
164
+ content: indexLines.join('\n'),
165
+ tokens: estimateTokens(indexLines.join('\n'))
166
+ });
167
+ }
168
+ // Generate individual doc files
169
+ for (const doc of skill.documents) {
170
+ let footer = '';
171
+ if (doc.apiRefs && doc.apiRefs.length > 0) {
172
+ footer = '\n\n---\n\n**See also:** ' + doc.apiRefs.map((r) => `\`${r}\``).join(', ');
173
+ }
174
+ const hasHeading = /^#\s/.test(doc.content);
175
+ const content = hasHeading
176
+ ? `${doc.content}${footer}`
177
+ : `# ${doc.title}\n\n${doc.content}${footer}`;
178
+ const subdir = doc.category ? `docs/${doc.category}` : 'docs';
179
+ references.push({
180
+ filename: `${basePath}/references/${subdir}/${toFilename(doc.title)}.md`,
181
+ content: truncateToTokenBudget(content, opts.maxTokens),
182
+ tokens: estimateTokens(content)
183
+ });
184
+ }
185
+ }
186
+ if (skill.configSurfaces && skill.configSurfaces.length > 0) {
187
+ const cliSurfaces = skill.configSurfaces.filter((s) => s.sourceType === 'cli');
188
+ const configSurfaces = skill.configSurfaces.filter((s) => s.sourceType !== 'cli');
189
+ if (cliSurfaces.length > 0) {
190
+ const content = renderConfigReference(cliSurfaces);
191
+ references.push({
192
+ filename: `${basePath}/references/commands.md`,
193
+ content: truncateToTokenBudget(content, opts.maxTokens),
194
+ tokens: estimateTokens(content)
195
+ });
196
+ }
197
+ if (configSurfaces.length > 0) {
198
+ const content = renderConfigReference(configSurfaces);
199
+ references.push({
200
+ filename: `${basePath}/references/config.md`,
201
+ content: truncateToTokenBudget(content, opts.maxTokens),
202
+ tokens: estimateTokens(content)
203
+ });
204
+ }
205
+ }
206
+ // MCP resources and prompts — emitted via shared helpers so every code path
207
+ // (default + invocation adapters that delegate here) gets consistent output.
208
+ const resourcesRef = renderResourcesReference(skill.resources ?? [], {
209
+ skillName: basePath,
210
+ maxTokens: opts.maxTokens
211
+ });
212
+ if (resourcesRef)
213
+ references.push(resourcesRef);
214
+ const promptsRef = renderPromptsReference(skill.prompts ?? [], {
215
+ skillName: basePath,
216
+ maxTokens: opts.maxTokens
217
+ });
218
+ if (promptsRef)
219
+ references.push(promptsRef);
220
+ const refManifest = buildRefManifest(basePath, references);
221
+ const skillContent = renderSkillMd(skill, skillName, opts, refManifest);
222
+ const result = {
223
+ skill: {
224
+ filename: `${basePath}/SKILL.md`,
225
+ content: skillContent,
226
+ tokens: estimateTokens(skillContent)
227
+ },
228
+ references
229
+ };
230
+ // Default canonicalize unless the caller explicitly opts out — adapters that
231
+ // mutate references after the inner renderSkill returns (e.g. target-mcpc
232
+ // appending its own tools.md) pass `canonicalize: false` so canonicalization
233
+ // runs exactly once at the host's outer wrapper.
234
+ return opts.canonicalize === false ? result : canonicalize(result);
235
+ }
236
+ // ===========================================================================
237
+ // Router skill — generated for monorepos with 2+ packages
238
+ // ===========================================================================
239
+ function renderRouterSkill(skills, opts) {
240
+ const names = skills.map((s) => s.name);
241
+ const scope = names[0]?.match(/^@([^/]+)\//)?.[1];
242
+ const routerName = scope ?? names.reduce((a, b) => (a.length < b.length ? a : b));
243
+ const skillName = toSkillName(routerName);
244
+ if (skills.some((s) => toSkillName(s.name) === skillName))
245
+ return null;
246
+ // --- Description: WHEN triggers + domain keywords ---
247
+ const pkgNames = skills.map((s) => s.name.replace(/^@[^/]+\//, '')).join(', ');
248
+ const allKeywords = [
249
+ ...new Set(skills
250
+ .flatMap((s) => s.keywords ?? [])
251
+ .filter((k) => !['typescript', 'javascript', 'node', 'nodejs', 'npm', 'library', 'package'].includes(k.toLowerCase())))
252
+ ];
253
+ const keywordSuffix = allKeywords.length > 0 ? ` Covers: ${allKeywords.slice(0, 10).join(', ')}.` : '';
254
+ const description = `Use when working with ${routerName} (${pkgNames}).${keywordSuffix}`;
255
+ const infos = skills.map((s) => {
256
+ // Key exports: top 3 classes + top 3 functions by name
257
+ const topClasses = s.classes.slice(0, 3).map((c) => `\`${c.name}\``);
258
+ const topFunctions = s.functions.slice(0, 3).map((f) => `\`${f.name}\``);
259
+ return {
260
+ short: s.name.replace(/^@[^/]+\//, ''),
261
+ peerName: toSkillName(s.name),
262
+ desc: s.packageDescription || s.description || '',
263
+ remarks: s.remarks || '',
264
+ useWhens: s.useWhen ?? [],
265
+ avoidWhens: s.avoidWhen ?? [],
266
+ nevers: s.pitfalls ?? [],
267
+ keyExports: [...topClasses, ...topFunctions].slice(0, 5)
268
+ };
269
+ });
270
+ const lines = [];
271
+ lines.push(renderFrontmatter(skillName, description, opts.license));
272
+ // --- Assertive opening ---
273
+ lines.push(`# ${routerName}`);
274
+ lines.push('');
275
+ lines.push(`**Use this skill for ANY work with ${routerName}.** It routes to the correct package.`);
276
+ lines.push('');
277
+ // --- When to Use: broad triggers ---
278
+ lines.push('## When to Use');
279
+ lines.push('');
280
+ lines.push('Use this router when:');
281
+ for (const info of infos) {
282
+ lines.push(`- ${info.desc}`);
283
+ }
284
+ lines.push('');
285
+ // --- Decision Tree: numbered quick routing ---
286
+ lines.push('## Decision Tree');
287
+ lines.push('');
288
+ for (let i = 0; i < infos.length; i++) {
289
+ const info = infos[i];
290
+ lines.push(`${i + 1}. ${info.desc}? → \`${info.peerName}\``);
291
+ }
292
+ lines.push('');
293
+ // --- Routing Logic: per-package detail with expert context ---
294
+ lines.push('## Routing Logic');
295
+ lines.push('');
296
+ for (const info of infos) {
297
+ lines.push(`### ${info.short} → \`${info.peerName}\``);
298
+ lines.push('');
299
+ // Expert intro from @remarks (thinking framework)
300
+ if (info.remarks) {
301
+ const firstPara = info.remarks.split(/\n\s*\n/)[0]?.trim();
302
+ if (firstPara && firstPara.length > 20) {
303
+ lines.push(firstPara);
304
+ lines.push('');
305
+ }
306
+ }
307
+ // @useWhen detail (only place these appear)
308
+ if (info.useWhens.length > 0) {
309
+ for (const t of info.useWhens.slice(0, 3)) {
310
+ lines.push(`- ${t}`);
311
+ }
312
+ lines.push('');
313
+ }
314
+ // Key exports
315
+ if (info.keyExports.length > 0) {
316
+ lines.push(`Key APIs: ${info.keyExports.join(', ')}`);
317
+ lines.push('');
318
+ }
319
+ }
320
+ // --- Critical Patterns: top NEVER per package (cross-cutting expert knowledge) ---
321
+ const crossCuttingNevers = [];
322
+ for (const info of infos) {
323
+ if (info.nevers.length > 0) {
324
+ crossCuttingNevers.push(`- ${info.nevers[0]} (${info.short})`);
325
+ }
326
+ }
327
+ if (crossCuttingNevers.length > 0) {
328
+ lines.push('## Critical Patterns');
329
+ lines.push('');
330
+ lines.push('Top pitfall per package:');
331
+ lines.push(...crossCuttingNevers);
332
+ lines.push('');
333
+ }
334
+ // --- Anti-Rationalization (from @avoidWhen) ---
335
+ const rationalizations = [];
336
+ for (const info of infos) {
337
+ if (info.avoidWhens.length > 0) {
338
+ const thought = info.avoidWhens[0];
339
+ rationalizations.push(`| "I'll just use ${info.short} for everything" | ${info.short} is for ${info.desc.toLowerCase()}. ${thought} |`);
340
+ }
341
+ }
342
+ if (rationalizations.length > 0) {
343
+ lines.push('## Anti-Rationalization');
344
+ lines.push('');
345
+ lines.push('| Thought | Reality |');
346
+ lines.push('|---------|---------|');
347
+ lines.push(...rationalizations);
348
+ lines.push('');
349
+ }
350
+ // --- Example Invocations ---
351
+ lines.push('## Example Invocations');
352
+ lines.push('');
353
+ for (const info of infos) {
354
+ lines.push(`User: "I need to ${info.desc.toLowerCase()}" `);
355
+ lines.push(`→ Load \`${info.peerName}\``);
356
+ lines.push('');
357
+ }
358
+ // --- NEVER ---
359
+ lines.push('## NEVER');
360
+ lines.push('');
361
+ lines.push('- NEVER load all package skills simultaneously — pick the one matching your task');
362
+ if (skills.length > 2) {
363
+ lines.push('- If your task spans multiple packages, load the foundational one first (typically core/shared), then the specific one');
364
+ }
365
+ lines.push('');
366
+ const content = lines.join('\n');
367
+ return {
368
+ skill: { filename: `${skillName}/SKILL.md`, content, tokens: estimateTokens(content) },
369
+ references: []
370
+ };
371
+ }
372
+ // ===========================================================================
373
+ // SKILL.md — lean discovery document
374
+ // ===========================================================================
375
+ function renderSkillMd(skill, skillName, opts, refManifest) {
376
+ const sections = [];
377
+ const description = buildDescription(skill);
378
+ sections.push(renderFrontmatter(skillName, description, opts.license || skill.license || '', opts.additionalFrontmatter));
379
+ // Body prefix injection — CLI-as-proxy adapters thread Setup instructions
380
+ // through here so the host install/connect commands appear before the
381
+ // standard skill body. Empty string is a no-op (sections.join skips it
382
+ // because it's still added but produces no visible effect when blank).
383
+ if (opts.bodyPrefix && opts.bodyPrefix.length > 0) {
384
+ sections.push(opts.bodyPrefix);
385
+ }
386
+ sections.push(`# ${skill.name}`);
387
+ // Package description as body intro
388
+ if (skill.packageDescription) {
389
+ sections.push(skill.packageDescription);
390
+ }
391
+ else if (skill.description) {
392
+ sections.push(skill.description);
393
+ }
394
+ // @remarks from @packageDocumentation — architectural context, trade-offs, mental models
395
+ if (skill.remarks) {
396
+ sections.push(skill.remarks);
397
+ }
398
+ // Features section from README — inline in SKILL.md
399
+ if (skill.readmeFeatures) {
400
+ sections.push('## Features\n\n' + skill.readmeFeatures);
401
+ }
402
+ // Quick Start example — when too long, extract just the first code block
403
+ if (opts.includeExamples && skill.examples.length > 0) {
404
+ const qs = skill.examples[0];
405
+ const qsLines = qs.split('\n');
406
+ if (qsLines.length > 30) {
407
+ // Extract first complete code block (``` ... ```)
408
+ const codeBlockMatch = qs.match(/```[\s\S]*?```/);
409
+ if (codeBlockMatch) {
410
+ sections.push('## Quick Start\n\n' + codeBlockMatch[0]);
411
+ }
412
+ else {
413
+ sections.push('## Quick Start\n\n' + qsLines.slice(0, 30).join('\n') + '\n...');
414
+ }
415
+ }
416
+ else {
417
+ sections.push('## Quick Start\n\n' + qs);
418
+ }
419
+ }
420
+ // Additional examples beyond Quick Start
421
+ if (opts.includeExamples && skill.examples.length > 1) {
422
+ const lines = ['## Examples\n'];
423
+ for (const ex of skill.examples.slice(1)) {
424
+ lines.push(ex);
425
+ lines.push('\n---\n');
426
+ }
427
+ sections.push(lines.join('\n'));
428
+ }
429
+ const whenToUse = renderWhenToUse(skill);
430
+ if (whenToUse)
431
+ sections.push(whenToUse);
432
+ const neverRules = renderNeverRules(skill);
433
+ if (neverRules)
434
+ sections.push(neverRules);
435
+ // Troubleshooting section from README — inline in SKILL.md
436
+ if (skill.readmeTroubleshooting) {
437
+ sections.push('## Troubleshooting\n\n' + skill.readmeTroubleshooting);
438
+ }
439
+ const configSection = renderConfigSurfaceSection(skill.configSurfaces);
440
+ if (configSection)
441
+ sections.push(configSection);
442
+ const quickRef = renderQuickReference(skill);
443
+ if (quickRef)
444
+ sections.push(quickRef);
445
+ const docs = renderDocumentation(skill);
446
+ if (docs)
447
+ sections.push(docs);
448
+ if (refManifest && Object.keys(refManifest).length > 0) {
449
+ sections.push(renderLoadingTriggers(refManifest));
450
+ }
451
+ const links = renderLinks(skill);
452
+ if (links)
453
+ sections.push(links);
454
+ return sections.join('\n\n');
455
+ }
456
+ // ===========================================================================
457
+ // Reference files — full detail loaded on demand
458
+ // ===========================================================================
459
+ /** Format description suffix — returns " — desc" or empty string, never trailing " — " */
460
+ function descSuffix(description) {
461
+ return description ? ` — ${description}` : '';
462
+ }
463
+ function getGroupKey(item) {
464
+ return item.category || item.sourceModule || '';
465
+ }
466
+ function groupByCategory(items) {
467
+ const groups = new Map();
468
+ for (const item of items) {
469
+ const key = getGroupKey(item);
470
+ const existing = groups.get(key);
471
+ if (existing)
472
+ existing.push(item);
473
+ else
474
+ groups.set(key, [item]);
475
+ }
476
+ return groups;
477
+ }
478
+ function hasGroupInfo(items) {
479
+ return items.some((item) => !!item.category || !!item.sourceModule);
480
+ }
481
+ // Keep old names as aliases for backward compatibility within this file
482
+ const groupByModule = groupByCategory;
483
+ const hasModuleInfo = hasGroupInfo;
484
+ function renderFunctionBody(fn, opts, lines) {
485
+ if (fn.description)
486
+ lines.push(fn.description);
487
+ if (fn.remarks) {
488
+ lines.push('');
489
+ lines.push(fn.remarks);
490
+ }
491
+ if (opts.includeSignatures && fn.signature) {
492
+ lines.push('```ts', fn.signature, '```');
493
+ }
494
+ if (fn.parameters.length > 0) {
495
+ lines.push('**Parameters:**');
496
+ for (const p of fn.parameters) {
497
+ const opt = p.optional ? ' (optional)' : '';
498
+ const def = p.defaultValue ? ` — default: \`${p.defaultValue}\`` : '';
499
+ lines.push(`- \`${p.name}: ${p.type}\`${opt}${def}${descSuffix(p.description)}`);
500
+ }
501
+ }
502
+ if (fn.returnType && fn.returnType !== 'void') {
503
+ const desc = fn.returnsDescription ? ` — ${fn.returnsDescription}` : '';
504
+ lines.push(`**Returns:** \`${fn.returnType}\`${desc}`);
505
+ }
506
+ // Render important tags
507
+ if (fn.tags['deprecated']) {
508
+ lines.push(`> **Deprecated:** ${fn.tags['deprecated']}`);
509
+ }
510
+ if (fn.tags['since']) {
511
+ lines.push(`**Since:** \`${fn.tags['since']}\``);
512
+ }
513
+ if (fn.tags['throws']) {
514
+ lines.push(`**Throws:** ${fn.tags['throws']}`);
515
+ }
516
+ if (fn.tags['see']) {
517
+ lines.push(`**See:** ${fn.tags['see']}`);
518
+ }
519
+ if (opts.includeSignatures && fn.overloads && fn.overloads.length > 0) {
520
+ lines.push('**Overloads:**');
521
+ for (const overload of fn.overloads) {
522
+ lines.push('```ts', overload, '```');
523
+ }
524
+ }
525
+ if (opts.includeExamples && fn.examples.length > 0) {
526
+ for (const ex of fn.examples) {
527
+ lines.push(ex);
528
+ }
529
+ }
530
+ lines.push('');
531
+ }
532
+ function renderFunctionsRef(fns, opts) {
533
+ const lines = ['# Functions\n'];
534
+ if (hasModuleInfo(fns)) {
535
+ const groups = groupByModule(fns);
536
+ for (const [mod, modFns] of groups) {
537
+ if (mod)
538
+ lines.push(`## ${mod}\n`);
539
+ for (const fn of modFns) {
540
+ lines.push(mod ? `### \`${fn.name}\`` : `## \`${fn.name}\``);
541
+ renderFunctionBody(fn, opts, lines);
542
+ }
543
+ }
544
+ }
545
+ else {
546
+ for (const fn of fns) {
547
+ lines.push(`## \`${fn.name}\``);
548
+ renderFunctionBody(fn, opts, lines);
549
+ }
550
+ }
551
+ return lines.join('\n');
552
+ }
553
+ function renderClassBody(cls, opts, lines, parentPropNames) {
554
+ if (cls.description)
555
+ lines.push(cls.description);
556
+ if (cls.extends) {
557
+ lines.push(`*extends \`${cls.extends}\`*`);
558
+ }
559
+ if (cls.implements && cls.implements.length > 0) {
560
+ lines.push(`*implements ${cls.implements.map((i) => `\`${i}\``).join(', ')}*`);
561
+ }
562
+ if (opts.includeSignatures && cls.constructorSignature) {
563
+ lines.push('```ts', cls.constructorSignature, '```');
564
+ }
565
+ if (cls.properties.length > 0) {
566
+ const ownProps = parentPropNames
567
+ ? cls.properties.filter((p) => !parentPropNames.has(p.name))
568
+ : cls.properties;
569
+ const inheritedCount = cls.properties.length - ownProps.length;
570
+ if (ownProps.length > 0) {
571
+ lines.push('**Properties:**');
572
+ for (const p of ownProps) {
573
+ const opt = p.optional ? ' (optional)' : '';
574
+ lines.push(`- \`${p.name}: ${p.type}\`${opt}${descSuffix(p.description)}`);
575
+ }
576
+ }
577
+ if (inheritedCount > 0 && cls.extends) {
578
+ const parentSlug = cls.extends
579
+ .toLowerCase()
580
+ .replace(/[^a-z0-9]+/g, '-')
581
+ .replace(/^-|-$/g, '');
582
+ lines.push(`*Inherits ${inheritedCount} properties from \`${cls.extends}\` — see [\`${cls.extends}\`](../${parentSlug}.md)*`);
583
+ }
584
+ }
585
+ if (cls.methods.length > 0) {
586
+ lines.push('**Methods:**');
587
+ for (const m of cls.methods) {
588
+ if (opts.includeSignatures) {
589
+ lines.push(`- \`${m.signature}\`${descSuffix(m.description)}`);
590
+ }
591
+ else {
592
+ lines.push(`- \`${m.name}\`${descSuffix(m.description)}`);
593
+ }
594
+ }
595
+ }
596
+ if (opts.includeExamples && cls.examples.length > 0) {
597
+ for (const ex of cls.examples) {
598
+ lines.push(ex);
599
+ }
600
+ }
601
+ lines.push('');
602
+ }
603
+ function renderClassesRef(classes, opts) {
604
+ const lines = ['# Classes\n'];
605
+ // Build a map of class name → property name set for deduplication
606
+ const classPropMap = new Map();
607
+ for (const cls of classes) {
608
+ classPropMap.set(cls.name, new Set(cls.properties.map((p) => p.name)));
609
+ }
610
+ function getParentProps(cls) {
611
+ if (!cls.extends)
612
+ return undefined;
613
+ return classPropMap.get(cls.extends);
614
+ }
615
+ if (hasModuleInfo(classes)) {
616
+ const groups = groupByModule(classes);
617
+ for (const [mod, modClasses] of groups) {
618
+ if (mod)
619
+ lines.push(`## ${mod}\n`);
620
+ for (const cls of modClasses) {
621
+ lines.push(mod ? `### \`${cls.name}\`` : `## \`${cls.name}\``);
622
+ renderClassBody(cls, opts, lines, getParentProps(cls));
623
+ }
624
+ }
625
+ }
626
+ else {
627
+ for (const cls of classes) {
628
+ lines.push(`## \`${cls.name}\``);
629
+ renderClassBody(cls, opts, lines, getParentProps(cls));
630
+ }
631
+ }
632
+ return lines.join('\n');
633
+ }
634
+ function renderTypesRef(types, enums) {
635
+ const lines = ['# Types & Enums\n'];
636
+ const allTypeLike = [...types, ...enums];
637
+ if (hasModuleInfo(allTypeLike)) {
638
+ // Group types and enums together by module
639
+ const typeGroups = groupByModule(types);
640
+ const enumGroups = groupByModule(enums);
641
+ const allMods = new Set([...typeGroups.keys(), ...enumGroups.keys()]);
642
+ for (const mod of allMods) {
643
+ if (mod)
644
+ lines.push(`## ${mod}\n`);
645
+ const heading = (name) => (mod ? `### \`${name}\`` : `## \`${name}\``);
646
+ const modTypes = typeGroups.get(mod) ?? [];
647
+ for (const t of modTypes) {
648
+ lines.push(heading(t.name));
649
+ if (t.description)
650
+ lines.push(t.description);
651
+ if (t.properties && t.properties.length > 0) {
652
+ lines.push('**Properties:**');
653
+ for (const p of t.properties) {
654
+ const opt = p.optional ? ' (optional)' : '';
655
+ lines.push(`- \`${p.name}: ${p.type}\`${opt}${descSuffix(p.description)}`);
656
+ }
657
+ }
658
+ if (t.definition) {
659
+ lines.push('```ts', t.definition, '```');
660
+ }
661
+ lines.push('');
662
+ }
663
+ const modEnums = enumGroups.get(mod) ?? [];
664
+ for (const e of modEnums) {
665
+ lines.push(heading(e.name));
666
+ if (e.description)
667
+ lines.push(e.description);
668
+ for (const m of e.members) {
669
+ lines.push(`- \`${m.name}\` = \`${m.value}\`${descSuffix(m.description)}`);
670
+ }
671
+ lines.push('');
672
+ }
673
+ }
674
+ }
675
+ else {
676
+ if (types.length > 0) {
677
+ lines.push('## Types\n');
678
+ for (const t of types) {
679
+ lines.push(`### \`${t.name}\``);
680
+ if (t.description)
681
+ lines.push(t.description);
682
+ if (t.properties && t.properties.length > 0) {
683
+ lines.push('**Properties:**');
684
+ for (const p of t.properties) {
685
+ const opt = p.optional ? ' (optional)' : '';
686
+ lines.push(`- \`${p.name}: ${p.type}\`${opt}${descSuffix(p.description)}`);
687
+ }
688
+ }
689
+ if (t.definition) {
690
+ lines.push('```ts', t.definition, '```');
691
+ }
692
+ lines.push('');
693
+ }
694
+ }
695
+ if (enums.length > 0) {
696
+ lines.push('## Enums\n');
697
+ for (const e of enums) {
698
+ lines.push(`### \`${e.name}\``);
699
+ if (e.description)
700
+ lines.push(e.description);
701
+ for (const m of e.members) {
702
+ lines.push(`- \`${m.name}\` = \`${m.value}\`${descSuffix(m.description)}`);
703
+ }
704
+ lines.push('');
705
+ }
706
+ }
707
+ }
708
+ return lines.join('\n');
709
+ }
710
+ function renderVariablesRef(variables) {
711
+ const lines = ['# Variables & Constants\n'];
712
+ if (hasModuleInfo(variables)) {
713
+ const groups = groupByModule(variables);
714
+ for (const [mod, modVars] of groups) {
715
+ if (mod)
716
+ lines.push(`## ${mod}\n`);
717
+ for (const v of modVars) {
718
+ lines.push(mod ? `### \`${v.name}\`` : `## \`${v.name}\``);
719
+ if (v.description)
720
+ lines.push(v.description);
721
+ const keyword = v.isConst ? 'const' : 'let';
722
+ lines.push('```ts', `${keyword} ${v.name}: ${v.type}`, '```');
723
+ lines.push('');
724
+ }
725
+ }
726
+ }
727
+ else {
728
+ for (const v of variables) {
729
+ lines.push(`## \`${v.name}\``);
730
+ if (v.description)
731
+ lines.push(v.description);
732
+ const keyword = v.isConst ? 'const' : 'let';
733
+ lines.push('```ts', `${keyword} ${v.name}: ${v.type}`, '```');
734
+ lines.push('');
735
+ }
736
+ }
737
+ return lines.join('\n');
738
+ }
739
+ // ===========================================================================
740
+ // Shared helpers
741
+ // ===========================================================================
742
+ /**
743
+ * When rendered content exceeds the token budget, split into per-group reference files
744
+ * using @category or sourceModule. Falls back to a single truncated file if no groups.
745
+ */
746
+ function addGroupedReferences(items, basePath, kind, opts, renderFn, itemLabel, references) {
747
+ const fullContent = renderFn(items);
748
+ const fullTokens = estimateTokens(fullContent);
749
+ // If it fits in the budget, emit as one file
750
+ if (fullTokens <= opts.maxTokens) {
751
+ references.push({
752
+ filename: `${basePath}/references/${kind}.md`,
753
+ content: fullContent,
754
+ tokens: fullTokens
755
+ });
756
+ return;
757
+ }
758
+ // Try splitting by group (category or sourceModule)
759
+ const grouped = groupByCategory(items);
760
+ if (grouped.size <= 1) {
761
+ // No groups — truncate as before
762
+ references.push({
763
+ filename: `${basePath}/references/${kind}.md`,
764
+ content: truncateToTokenBudget(fullContent, opts.maxTokens),
765
+ tokens: fullTokens
766
+ });
767
+ return;
768
+ }
769
+ // Emit one file per group in a subdirectory: references/<kind>/<group>.md
770
+ // If a group still exceeds the budget, split into one file per item
771
+ for (const [groupName, groupItems] of grouped) {
772
+ const slug = groupName
773
+ ? groupName
774
+ .toLowerCase()
775
+ .replace(/[^a-z0-9/]+/g, '-')
776
+ .replace(/^-|-$/g, '')
777
+ : 'other';
778
+ const content = renderFn(groupItems);
779
+ const tokens = estimateTokens(content);
780
+ if (tokens <= opts.maxTokens || groupItems.length <= 1) {
781
+ references.push({
782
+ filename: `${basePath}/references/${kind}/${slug}.md`,
783
+ content: truncateToTokenBudget(content, opts.maxTokens),
784
+ tokens
785
+ });
786
+ }
787
+ else {
788
+ // Group still too large — split into one file per item
789
+ const indexRows = [];
790
+ for (const item of groupItems) {
791
+ const itemSlug = toSkillName(item.name);
792
+ const itemContent = renderFn([item]);
793
+ // Use 2× budget for individual item files — they're already scoped to one item
794
+ references.push({
795
+ filename: `${basePath}/references/${kind}/${slug}/${itemSlug}.md`,
796
+ content: truncateToTokenBudget(itemContent, opts.maxTokens * 2),
797
+ tokens: estimateTokens(itemContent)
798
+ });
799
+ indexRows.push(`| [${item.name}](${itemSlug}.md) | ${item.description} |`);
800
+ }
801
+ // Emit an index.md for this subdirectory
802
+ const indexContent = [
803
+ `# ${groupName || slug}`,
804
+ '',
805
+ `| ${itemLabel} | Description |`,
806
+ `|${'-'.repeat(itemLabel.length + 2)}|-------------|`,
807
+ ...indexRows
808
+ ].join('\n');
809
+ references.push({
810
+ filename: `${basePath}/references/${kind}/${slug}/index.md`,
811
+ content: indexContent,
812
+ tokens: estimateTokens(indexContent)
813
+ });
814
+ }
815
+ }
816
+ }
817
+ function buildRefManifest(basePath, references) {
818
+ const manifest = {};
819
+ const prefix = `${basePath}/references/`;
820
+ for (const ref of references) {
821
+ if (!ref.filename.startsWith(prefix))
822
+ continue;
823
+ const relative = ref.filename.slice(prefix.length);
824
+ if (relative.length === 0)
825
+ continue;
826
+ const segment = relative.split('/')[0] ?? '';
827
+ const category = segment.replace(/\.md$/, '');
828
+ if (!REF_CATEGORY_ORDER.includes(category))
829
+ continue;
830
+ const isFile = relative === `${category}.md`;
831
+ const entry = {
832
+ path: (isFile
833
+ ? `references/${category}.md`
834
+ : `references/${category}/`),
835
+ mode: isFile ? 'file' : 'directory'
836
+ };
837
+ const existing = manifest[category];
838
+ if (!existing || entry.mode === 'directory') {
839
+ manifest[category] = entry;
840
+ }
841
+ }
842
+ return manifest;
843
+ }
844
+ function buildDescription(skill) {
845
+ // Description = package.json tagline + short WHEN clause + domain keywords.
846
+ const desc = skill.packageDescription || skill.description || `API reference for ${skill.name}`;
847
+ const parts = [desc];
848
+ // Add a short scenario-based WHEN from @useWhen (first item only, truncated to 80 chars)
849
+ if (skill.useWhen && skill.useWhen.length > 0) {
850
+ const first = skill.useWhen[0];
851
+ const short = first.length > 80 ? first.slice(0, first.lastIndexOf(' ', 77)) + '...' : first;
852
+ parts.push(`Use when: ${short}.`);
853
+ }
854
+ // Append domain keywords for activation matching
855
+ if (skill.keywords && skill.keywords.length > 0) {
856
+ const useful = skill.keywords.filter((k) => !['typescript', 'javascript', 'node', 'nodejs', 'npm', 'library', 'package'].includes(k.toLowerCase()));
857
+ if (useful.length > 0) {
858
+ parts.push(`Also: ${useful.join(', ')}.`);
859
+ }
860
+ }
861
+ return truncateDescription(parts.join(' '), DESCRIPTION_MAX);
862
+ }
863
+ function truncateDescription(desc, max) {
864
+ if (desc.length <= max)
865
+ return desc;
866
+ // Match first sentence — but skip .!? inside backticks (e.g. `?z2f`)
867
+ // Also handle em-dash: treat "—" as transparent (not a sentence boundary)
868
+ const firstSentence = desc.match(/^(?:[^.!?`]|`[^`]*`)+[.!?]/)?.[0];
869
+ if (firstSentence && firstSentence.length <= max)
870
+ return firstSentence;
871
+ // Fallback: cut at last space before max to avoid mid-word truncation
872
+ const cutpoint = desc.lastIndexOf(' ', max - 4);
873
+ return desc.slice(0, cutpoint > 0 ? cutpoint : max - 3) + '...';
874
+ }
875
+ function renderFrontmatter(name, description, license, additional) {
876
+ const lines = ['---', `name: ${name}`];
877
+ lines.push(`description: ${quoteYaml(description)}`);
878
+ if (license)
879
+ lines.push(`license: ${license}`);
880
+ if (additional) {
881
+ // Track existing keys so additional values cannot overwrite them — collisions
882
+ // silently keep the existing key (e.g. user passing `name` in additional is a no-op).
883
+ const existing = new Set(['name', 'description']);
884
+ if (license)
885
+ existing.add('license');
886
+ for (const [key, value] of Object.entries(additional)) {
887
+ if (existing.has(key))
888
+ continue;
889
+ // Serialize via yaml so nested objects/arrays produce proper block output.
890
+ // We trim the trailing newline that yaml.stringify appends so our caller
891
+ // can join with '\n' cleanly. Indented child lines retain their indent.
892
+ const serialized = YAML.stringify({ [key]: value }).replace(/\n$/, '');
893
+ lines.push(serialized);
894
+ existing.add(key);
895
+ }
896
+ }
897
+ lines.push('---');
898
+ return lines.join('\n');
899
+ }
900
+ function quoteYaml(value) {
901
+ if (/[:#{}[\],&*?|>!%@`"']/.test(value) || value.includes('\n')) {
902
+ return `"${value.replace(/"/g, '\\"')}"`;
903
+ }
904
+ return value;
905
+ }
906
+ function extractFirstSentence(text) {
907
+ const match = text.match(/^[^.!?]*[.!?]/);
908
+ return match ? match[0].trim() : '';
909
+ }
910
+ function renderDocumentation(skill) {
911
+ if (!skill.documents || skill.documents.length === 0)
912
+ return '';
913
+ const lines = ['## Documentation\n'];
914
+ // Group by category
915
+ const categorized = new Map();
916
+ const uncategorized = [];
917
+ for (const doc of skill.documents) {
918
+ if (doc.category) {
919
+ const list = categorized.get(doc.category) ?? [];
920
+ list.push(doc);
921
+ categorized.set(doc.category, list);
922
+ }
923
+ else {
924
+ uncategorized.push(doc);
925
+ }
926
+ }
927
+ if (categorized.size > 1) {
928
+ // Progressive disclosure: category summary → index files → individual docs
929
+ // Use parent doc's description for category summary, fall back to doc title list
930
+ for (const [category, docs] of categorized) {
931
+ const parentDoc = docs.find((d) => d.isParent);
932
+ const desc = parentDoc?.description ?? docs.map((d) => d.title).join(', ');
933
+ lines.push(`- **${category}** (${docs.length}) — ${desc}`);
934
+ }
935
+ if (uncategorized.length > 0) {
936
+ for (const doc of uncategorized) {
937
+ const desc = doc.description ?? extractFirstSentence(doc.content);
938
+ lines.push(`- **${doc.title}**${desc ? ` — ${desc}` : ''}`);
939
+ }
940
+ }
941
+ }
942
+ else {
943
+ // Few docs — flat list with descriptions
944
+ for (const doc of skill.documents) {
945
+ const desc = doc.description ?? extractFirstSentence(doc.content);
946
+ lines.push(`- **${doc.title}**${desc ? ` — ${desc}` : ''}`);
947
+ }
948
+ }
949
+ lines.push('');
950
+ lines.push(`See \`references/docs/\` for full guides (${skill.documents.length} total).`);
951
+ return lines.join('\n');
952
+ }
953
+ /** Scenario-based loading triggers for reference files */
954
+ function renderLoadingTriggers(manifest) {
955
+ const triggerMap = {
956
+ functions: {
957
+ file: (path) => `When calling any function → read \`${path}\` for full signatures, parameters, and return types`,
958
+ directory: (path) => `When calling any function → browse \`${path}\` for grouped indexes, full signatures, parameters, and return types`
959
+ },
960
+ classes: {
961
+ file: (path) => `When using a class → read \`${path}\` for properties, methods, and inheritance`,
962
+ directory: (path) => `When using a class → browse \`${path}\` for grouped indexes, properties, methods, and inheritance`
963
+ },
964
+ types: {
965
+ file: (path) => `When defining typed variables or function parameters → read \`${path}\``,
966
+ directory: (path) => `When defining typed variables or function parameters → browse \`${path}\``
967
+ },
968
+ variables: {
969
+ file: (path) => `When using exported constants → read \`${path}\``,
970
+ directory: (path) => `When using exported constants → browse \`${path}\``
971
+ },
972
+ commands: {
973
+ file: (path) => `When using CLI commands → read \`${path}\` for flags, arguments, and defaults`,
974
+ directory: (path) => `When using CLI commands → browse \`${path}\` for grouped command references`
975
+ },
976
+ config: {
977
+ file: (path) => `When configuring options → read \`${path}\` for all settings and defaults`,
978
+ directory: (path) => `When configuring options → browse \`${path}\` for grouped settings`
979
+ },
980
+ docs: {
981
+ file: (path) => `When learning concepts or workflows → read \`${path}\``,
982
+ directory: (path) => `When learning concepts or workflows → browse \`${path}\` by category`
983
+ },
984
+ resources: {
985
+ file: (path) => `When you need to read MCP-exposed resources → read \`${path}\` for URI templates and MIME types`,
986
+ directory: (path) => `When you need to read MCP-exposed resources → browse \`${path}\` for URI templates and MIME types`
987
+ },
988
+ prompts: {
989
+ file: (path) => `When invoking MCP-exposed prompts → read \`${path}\` for arguments and prompt names`,
990
+ directory: (path) => `When invoking MCP-exposed prompts → browse \`${path}\` for arguments and prompt names`
991
+ },
992
+ examples: {
993
+ file: (path) => `For additional usage patterns → read \`${path}\``,
994
+ directory: (path) => `For additional usage patterns → browse \`${path}\``
995
+ }
996
+ };
997
+ const lines = ['## References\n'];
998
+ lines.push('Load these on demand — do NOT read all at once:\n');
999
+ for (const category of REF_CATEGORY_ORDER) {
1000
+ const entry = manifest[category];
1001
+ if (!entry)
1002
+ continue;
1003
+ const trigger = entry.mode === 'directory'
1004
+ ? triggerMap[category].directory(entry.path)
1005
+ : triggerMap[category].file(entry.path);
1006
+ lines.push(`- ${trigger}`);
1007
+ }
1008
+ return lines.join('\n');
1009
+ }
1010
+ function renderLinks(skill) {
1011
+ const links = [];
1012
+ if (skill.repository)
1013
+ links.push(`- [Repository](${skill.repository})`);
1014
+ if (skill.author)
1015
+ links.push(`- Author: ${skill.author}`);
1016
+ if (links.length === 0)
1017
+ return '';
1018
+ return '## Links\n\n' + links.join('\n');
1019
+ }
1020
+ function renderWhenToUse(skill) {
1021
+ const useLines = [];
1022
+ const avoidLines = [];
1023
+ // @useWhen — bullet list with source attribution when multiple sources exist
1024
+ if (skill.useWhenSources && skill.useWhenSources.length > 0) {
1025
+ const sources = skill.useWhenSources;
1026
+ const distinctSources = new Set(sources.map((s) => s.sourceName));
1027
+ for (const src of sources) {
1028
+ const attribution = distinctSources.size > 1 ? ` → use \`${src.sourceName}\`` : '';
1029
+ const dashIdx = src.text.indexOf(' — ');
1030
+ if (dashIdx !== -1) {
1031
+ useLines.push(`- ${src.text.slice(0, dashIdx).trim()}${attribution} — ${src.text.slice(dashIdx + 3).trim()}`);
1032
+ }
1033
+ else {
1034
+ useLines.push(`- ${src.text}${attribution}`);
1035
+ }
1036
+ }
1037
+ }
1038
+ else if (skill.useWhen && skill.useWhen.length > 0) {
1039
+ for (const item of skill.useWhen) {
1040
+ useLines.push(`- ${item}`);
1041
+ }
1042
+ }
1043
+ // @avoidWhen — bullet list
1044
+ if (skill.avoidWhenSources && skill.avoidWhenSources.length > 0) {
1045
+ const sources = skill.avoidWhenSources;
1046
+ const distinctSources = new Set(sources.map((s) => s.sourceName));
1047
+ for (const src of sources) {
1048
+ const attribution = distinctSources.size > 1 ? ` (\`${src.sourceName}\`)` : '';
1049
+ avoidLines.push(`- ${src.text}${attribution}`);
1050
+ }
1051
+ }
1052
+ else if (skill.avoidWhen && skill.avoidWhen.length > 0) {
1053
+ for (const item of skill.avoidWhen) {
1054
+ avoidLines.push(`- ${item}`);
1055
+ }
1056
+ }
1057
+ if (useLines.length === 0 && avoidLines.length === 0)
1058
+ return '';
1059
+ const sections = [];
1060
+ if (useLines.length > 0) {
1061
+ sections.push('**Use this skill when:**\n' + useLines.join('\n'));
1062
+ }
1063
+ if (avoidLines.length > 0) {
1064
+ sections.push('**Do NOT use when:**\n' + avoidLines.join('\n'));
1065
+ }
1066
+ // API surface summary
1067
+ const apiCategories = [];
1068
+ if (skill.functions.length > 0)
1069
+ apiCategories.push(`${skill.functions.length} functions`);
1070
+ if (skill.classes.length > 0)
1071
+ apiCategories.push(`${skill.classes.length} classes`);
1072
+ if (skill.types.length > 0)
1073
+ apiCategories.push(`${skill.types.length} types`);
1074
+ if (skill.enums.length > 0)
1075
+ apiCategories.push(`${skill.enums.length} enums`);
1076
+ if (skill.variables && skill.variables.length > 0)
1077
+ apiCategories.push(`${skill.variables.length} constants`);
1078
+ if (apiCategories.length > 0) {
1079
+ sections.push(`API surface: ${apiCategories.join(', ')}`);
1080
+ }
1081
+ return '## When to Use\n\n' + sections.join('\n\n');
1082
+ }
1083
+ function renderNeverRules(skill) {
1084
+ if (!skill.pitfalls || skill.pitfalls.length === 0)
1085
+ return '';
1086
+ const lines = [];
1087
+ for (const item of skill.pitfalls) {
1088
+ lines.push(`- ${item}`);
1089
+ }
1090
+ return '## NEVER\n\n' + lines.join('\n');
1091
+ }
1092
+ /** Extract the first sentence (or first meaningful phrase) from a description */
1093
+ function firstSentence(desc) {
1094
+ if (!desc)
1095
+ return '';
1096
+ const match = desc.match(/^[^.!?]*[.!?]/);
1097
+ if (match)
1098
+ return match[0].trim().replace(/[.!?]$/, '');
1099
+ // No sentence terminator — use up to first comma or 60 chars
1100
+ const commaIdx = desc.indexOf(',');
1101
+ if (commaIdx > 0 && commaIdx <= 60)
1102
+ return desc.slice(0, commaIdx).trim();
1103
+ if (desc.length <= 60)
1104
+ return desc.trim();
1105
+ return desc.slice(0, 57).trim() + '...';
1106
+ }
1107
+ /** Format an item as "`Name` (desc)" — omit desc when empty */
1108
+ function compactItem(name, description) {
1109
+ const label = firstSentence(description);
1110
+ return label ? `\`${name}\` (${label})` : `\`${name}\``;
1111
+ }
1112
+ /** Max lines of Quick Reference content (after heading) before truncation */
1113
+ const QUICK_REF_MAX_LINES = 30;
1114
+ const QUICK_REF_KEEP_LINES = 25;
1115
+ function renderQuickReference(skill) {
1116
+ const allItems = [
1117
+ ...skill.functions,
1118
+ ...skill.classes,
1119
+ ...skill.types,
1120
+ ...skill.enums,
1121
+ ...(skill.variables ?? [])
1122
+ ];
1123
+ if (allItems.length === 0)
1124
+ return '';
1125
+ const totalCount = allItems.length;
1126
+ // For large packages (30+ exports): only show "important" items — those with
1127
+ // @useWhen, @category, or @remarks tags (author marked them as key decision points).
1128
+ // Dump the rest with just a count pointing to references/.
1129
+ const isLargePackage = totalCount > 30;
1130
+ const isImportant = (item) => {
1131
+ if (item.category)
1132
+ return true;
1133
+ if (item.remarks)
1134
+ return true;
1135
+ if (item.tags?.['useWhen'] || item.tags?.['avoidWhen'] || item.tags?.['never'])
1136
+ return true;
1137
+ return false;
1138
+ };
1139
+ let contentLines;
1140
+ if (isLargePackage) {
1141
+ // Filter to important items only
1142
+ const importantItems = allItems.filter(isImportant);
1143
+ const importantFunctions = skill.functions.filter(isImportant);
1144
+ const importantClasses = skill.classes.filter(isImportant);
1145
+ if (importantItems.length > 0) {
1146
+ contentLines = [];
1147
+ if (importantFunctions.length > 0) {
1148
+ const entries = importantFunctions.map((f) => compactItem(f.name, f.description));
1149
+ contentLines.push(`**Key functions:** ${entries.join(', ')}`);
1150
+ }
1151
+ if (importantClasses.length > 0) {
1152
+ const entries = importantClasses.map((c) => compactItem(c.name, c.description));
1153
+ contentLines.push(`**Key classes:** ${entries.join(', ')}`);
1154
+ }
1155
+ contentLines.push('');
1156
+ contentLines.push(`*${totalCount} exports total — see references/ for full API.*`);
1157
+ }
1158
+ else {
1159
+ // No items marked as important — show a count-only summary
1160
+ const apiCategories = [];
1161
+ if (skill.functions.length > 0)
1162
+ apiCategories.push(`${skill.functions.length} functions`);
1163
+ if (skill.classes.length > 0)
1164
+ apiCategories.push(`${skill.classes.length} classes`);
1165
+ if (skill.types.length > 0)
1166
+ apiCategories.push(`${skill.types.length} types`);
1167
+ if (skill.enums.length > 0)
1168
+ apiCategories.push(`${skill.enums.length} enums`);
1169
+ if (skill.variables && skill.variables.length > 0)
1170
+ apiCategories.push(`${skill.variables.length} constants`);
1171
+ contentLines = [
1172
+ `${totalCount} exports (${apiCategories.join(', ')}) — see references/ for full API.`
1173
+ ];
1174
+ }
1175
+ }
1176
+ else if (hasModuleInfo(allItems)) {
1177
+ // Medium packages with module grouping — show all, grouped
1178
+ const groups = groupByModule(allItems);
1179
+ contentLines = [];
1180
+ for (const [mod, modItems] of groups) {
1181
+ const entries = modItems.map((item) => {
1182
+ const name = 'name' in item ? item.name : '';
1183
+ const desc = 'description' in item ? item.description : '';
1184
+ return compactItem(name, desc);
1185
+ });
1186
+ if (mod) {
1187
+ contentLines.push(`**${mod}:** ${entries.join(', ')}`);
1188
+ }
1189
+ else {
1190
+ contentLines.push(entries.join(', '));
1191
+ }
1192
+ }
1193
+ }
1194
+ else {
1195
+ // Small packages — show all, flat by kind
1196
+ contentLines = [];
1197
+ if (skill.functions.length > 0) {
1198
+ const entries = skill.functions.map((f) => compactItem(f.name, f.description));
1199
+ contentLines.push(`**Functions:** ${entries.join(', ')}`);
1200
+ }
1201
+ if (skill.classes.length > 0) {
1202
+ const entries = skill.classes.map((c) => compactItem(c.name, c.description));
1203
+ contentLines.push(`**Classes:** ${entries.join(', ')}`);
1204
+ }
1205
+ if (skill.types.length > 0) {
1206
+ const entries = skill.types.map((t) => compactItem(t.name, t.description));
1207
+ contentLines.push(`**Types:** ${entries.join(', ')}`);
1208
+ }
1209
+ if (skill.enums.length > 0) {
1210
+ const entries = skill.enums.map((e) => compactItem(e.name, e.description));
1211
+ contentLines.push(`**Enums:** ${entries.join(', ')}`);
1212
+ }
1213
+ if (skill.variables && skill.variables.length > 0) {
1214
+ const entries = skill.variables.map((v) => compactItem(v.name, v.description));
1215
+ contentLines.push(`**Constants:** ${entries.join(', ')}`);
1216
+ }
1217
+ }
1218
+ // Final cap as safety net
1219
+ let body = contentLines.join('\n');
1220
+ const renderedLines = body.split('\n');
1221
+ if (renderedLines.length > QUICK_REF_MAX_LINES) {
1222
+ body = renderedLines.slice(0, QUICK_REF_KEEP_LINES).join('\n');
1223
+ body += `\n\n*${totalCount} exports total — see references/ for full API.*`;
1224
+ }
1225
+ return '## Quick Reference\n\n' + body;
1226
+ }
1227
+ function toSkillName(name) {
1228
+ return name
1229
+ .replace(/^@/, '') // strip npm scope @
1230
+ .replace(/\//g, '-') // scope separator → hyphen
1231
+ .replace(/([a-z])([A-Z])/g, '$1-$2') // camelCase → kebab-case
1232
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // XMLParser → xml-parser
1233
+ .toLowerCase()
1234
+ .replace(/[^a-z0-9-]/g, '-') // strip invalid chars
1235
+ .replace(/-+/g, '-') // collapse hyphens
1236
+ .replace(/^-|-$/g, ''); // trim leading/trailing
1237
+ }
1238
+ function toFilename(title) {
1239
+ return title
1240
+ .toLowerCase()
1241
+ .replace(/[^a-z0-9]+/g, '-')
1242
+ .replace(/^-|-$/g, '');
1243
+ }
1244
+ //# sourceMappingURL=renderer.js.map