@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.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/audit-formatter.d.ts +36 -0
- package/dist/audit-formatter.d.ts.map +1 -0
- package/dist/audit-formatter.js +173 -0
- package/dist/audit-formatter.js.map +1 -0
- package/dist/audit-score.d.ts +76 -0
- package/dist/audit-score.d.ts.map +1 -0
- package/dist/audit-score.js +490 -0
- package/dist/audit-score.js.map +1 -0
- package/dist/audit-types.d.ts +81 -0
- package/dist/audit-types.d.ts.map +1 -0
- package/dist/audit-types.js +2 -0
- package/dist/audit-types.js.map +1 -0
- package/dist/audit.d.ts +17 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +567 -0
- package/dist/audit.js.map +1 -0
- package/dist/canonical.d.ts +36 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +148 -0
- package/dist/canonical.js.map +1 -0
- package/dist/config-renderer.d.ts +16 -0
- package/dist/config-renderer.d.ts.map +1 -0
- package/dist/config-renderer.js +296 -0
- package/dist/config-renderer.js.map +1 -0
- package/dist/config-types.d.ts +144 -0
- package/dist/config-types.d.ts.map +1 -0
- package/dist/config-types.js +2 -0
- package/dist/config-types.js.map +1 -0
- package/dist/docs-scanner.d.ts +40 -0
- package/dist/docs-scanner.d.ts.map +1 -0
- package/dist/docs-scanner.js +176 -0
- package/dist/docs-scanner.js.map +1 -0
- package/dist/examples-scanner.d.ts +39 -0
- package/dist/examples-scanner.d.ts.map +1 -0
- package/dist/examples-scanner.js +221 -0
- package/dist/examples-scanner.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/llms-txt.d.ts +20 -0
- package/dist/llms-txt.d.ts.map +1 -0
- package/dist/llms-txt.js +187 -0
- package/dist/llms-txt.js.map +1 -0
- package/dist/markdown-parser.d.ts +11 -0
- package/dist/markdown-parser.d.ts.map +1 -0
- package/dist/markdown-parser.js +222 -0
- package/dist/markdown-parser.js.map +1 -0
- package/dist/markdown-types.d.ts +48 -0
- package/dist/markdown-types.d.ts.map +1 -0
- package/dist/markdown-types.js +2 -0
- package/dist/markdown-types.js.map +1 -0
- package/dist/readme-parser.d.ts +21 -0
- package/dist/readme-parser.d.ts.map +1 -0
- package/dist/readme-parser.js +137 -0
- package/dist/readme-parser.js.map +1 -0
- package/dist/references-mcp.d.ts +79 -0
- package/dist/references-mcp.d.ts.map +1 -0
- package/dist/references-mcp.js +130 -0
- package/dist/references-mcp.js.map +1 -0
- package/dist/refine/ast-edit.d.ts +21 -0
- package/dist/refine/ast-edit.d.ts.map +1 -0
- package/dist/refine/ast-edit.js +147 -0
- package/dist/refine/ast-edit.js.map +1 -0
- package/dist/refine/index.d.ts +7 -0
- package/dist/refine/index.d.ts.map +1 -0
- package/dist/refine/index.js +6 -0
- package/dist/refine/index.js.map +1 -0
- package/dist/refine/jsdoc-edit.d.ts +4 -0
- package/dist/refine/jsdoc-edit.d.ts.map +1 -0
- package/dist/refine/jsdoc-edit.js +6 -0
- package/dist/refine/jsdoc-edit.js.map +1 -0
- package/dist/refine/loop.d.ts +8 -0
- package/dist/refine/loop.d.ts.map +1 -0
- package/dist/refine/loop.js +98 -0
- package/dist/refine/loop.js.map +1 -0
- package/dist/refine/select-targets.d.ts +5 -0
- package/dist/refine/select-targets.d.ts.map +1 -0
- package/dist/refine/select-targets.js +30 -0
- package/dist/refine/select-targets.js.map +1 -0
- package/dist/refine/types.d.ts +67 -0
- package/dist/refine/types.d.ts.map +1 -0
- package/dist/refine/types.js +2 -0
- package/dist/refine/types.js.map +1 -0
- package/dist/renderer.d.ts +27 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +1244 -0
- package/dist/renderer.js.map +1 -0
- package/dist/tokens.d.ts +14 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +26 -0
- package/dist/tokens.js.map +1 -0
- package/dist/types.d.ts +519 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/writer.d.ts +17 -0
- package/dist/writer.d.ts.map +1 -0
- package/dist/writer.js +313 -0
- package/dist/writer.js.map +1 -0
- package/package.json +54 -0
package/dist/renderer.js
ADDED
|
@@ -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
|