mdkg 0.0.2 → 0.0.4

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 (50) hide show
  1. package/README.md +171 -151
  2. package/dist/cli.js +920 -422
  3. package/dist/commands/checkpoint.js +17 -6
  4. package/dist/commands/doctor.js +156 -0
  5. package/dist/commands/event.js +46 -0
  6. package/dist/commands/event_support.js +146 -0
  7. package/dist/commands/format.js +6 -7
  8. package/dist/commands/index.js +10 -4
  9. package/dist/commands/init.js +202 -11
  10. package/dist/commands/list.js +18 -1
  11. package/dist/commands/new.js +30 -5
  12. package/dist/commands/pack.js +332 -10
  13. package/dist/commands/query_output.js +84 -0
  14. package/dist/commands/search.js +22 -5
  15. package/dist/commands/show.js +26 -11
  16. package/dist/commands/skill.js +359 -0
  17. package/dist/commands/skill_support.js +121 -0
  18. package/dist/commands/task.js +270 -0
  19. package/dist/commands/validate.js +104 -7
  20. package/dist/graph/edges.js +2 -2
  21. package/dist/graph/frontmatter.js +1 -0
  22. package/dist/graph/indexer.js +21 -0
  23. package/dist/graph/node.js +20 -4
  24. package/dist/graph/skills_index_cache.js +94 -0
  25. package/dist/graph/skills_indexer.js +160 -0
  26. package/dist/init/README.md +43 -0
  27. package/dist/init/core/rule-1-mdkg-conventions.md +9 -2
  28. package/dist/init/core/rule-3-cli-contract.md +73 -14
  29. package/dist/init/core/rule-4-repo-safety-and-ignores.md +9 -3
  30. package/dist/init/core/rule-6-templates-and-schemas.md +6 -2
  31. package/dist/init/skills/SKILL.md.example +41 -0
  32. package/dist/init/templates/default/bug.md +1 -0
  33. package/dist/init/templates/default/chk.md +1 -0
  34. package/dist/init/templates/default/epic.md +1 -0
  35. package/dist/init/templates/default/feat.md +1 -0
  36. package/dist/init/templates/default/task.md +1 -0
  37. package/dist/init/templates/default/test.md +1 -0
  38. package/dist/pack/budget.js +186 -0
  39. package/dist/pack/export_md.js +17 -1
  40. package/dist/pack/export_xml.js +15 -0
  41. package/dist/pack/metrics.js +66 -0
  42. package/dist/pack/pack.js +35 -0
  43. package/dist/pack/profile.js +222 -0
  44. package/dist/pack/stats.js +37 -0
  45. package/dist/templates/headings.js +34 -0
  46. package/dist/util/argparse.js +47 -1
  47. package/dist/util/filter.js +18 -0
  48. package/dist/util/id.js +23 -0
  49. package/dist/util/output.js +2 -2
  50. package/package.json +6 -2
@@ -0,0 +1,359 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runSkillNewCommand = runSkillNewCommand;
7
+ exports.runSkillListCommand = runSkillListCommand;
8
+ exports.runSkillShowCommand = runSkillShowCommand;
9
+ exports.runSkillSearchCommand = runSkillSearchCommand;
10
+ exports.runSkillValidateCommand = runSkillValidateCommand;
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const config_1 = require("../core/config");
14
+ const skills_indexer_1 = require("../graph/skills_indexer");
15
+ const skills_index_cache_1 = require("../graph/skills_index_cache");
16
+ const skills_index_cache_2 = require("../graph/skills_index_cache");
17
+ const errors_1 = require("../util/errors");
18
+ const skill_support_1 = require("./skill_support");
19
+ const query_output_1 = require("./query_output");
20
+ const event_support_1 = require("./event_support");
21
+ function parseCsvList(raw) {
22
+ if (!raw) {
23
+ return [];
24
+ }
25
+ return raw
26
+ .split(",")
27
+ .map((value) => value.trim())
28
+ .filter(Boolean);
29
+ }
30
+ function normalizeLowercaseList(raw) {
31
+ return parseCsvList(raw).map((value) => value.toLowerCase());
32
+ }
33
+ function normalizeSlug(raw) {
34
+ const slug = raw.trim().toLowerCase();
35
+ if (!skills_indexer_1.SKILL_SLUG_RE.test(slug)) {
36
+ throw new errors_1.UsageError(`skill slug must be kebab-case: ${raw}`);
37
+ }
38
+ return slug;
39
+ }
40
+ function resolveSkillPaths(root, slug) {
41
+ const config = (0, config_1.loadConfig)(root);
42
+ const skillsRoot = (0, skills_indexer_1.resolveSkillsRoot)(root, config);
43
+ const skillDir = path_1.default.join(skillsRoot, slug);
44
+ return {
45
+ skillDir,
46
+ canonicalPath: path_1.default.join(skillDir, "SKILL.md"),
47
+ compatPath: path_1.default.join(skillDir, "SKILLS.md"),
48
+ };
49
+ }
50
+ function validateSingleSkill(root, slug) {
51
+ const { skillDir, canonicalPath, compatPath } = resolveSkillPaths(root, slug);
52
+ const warnings = [];
53
+ const errors = [];
54
+ const hasCanonical = fs_1.default.existsSync(canonicalPath);
55
+ const hasCompat = fs_1.default.existsSync(compatPath);
56
+ if (!fs_1.default.existsSync(skillDir)) {
57
+ throw new errors_1.NotFoundError(`skill not found: ${slug}`);
58
+ }
59
+ if (hasCanonical && hasCompat) {
60
+ errors.push(`${skillDir}: both SKILL.md and SKILLS.md exist`);
61
+ return { warnings, errors };
62
+ }
63
+ if (!hasCanonical && !hasCompat) {
64
+ errors.push(`${skillDir}: missing SKILL.md or SKILLS.md`);
65
+ return { warnings, errors };
66
+ }
67
+ const skillPath = hasCanonical ? canonicalPath : compatPath;
68
+ if (!hasCanonical) {
69
+ warnings.push(`${path_1.default.relative(root, compatPath)}: using legacy SKILLS.md compatibility file`);
70
+ }
71
+ try {
72
+ (0, skills_indexer_1.buildSkillIndexEntry)(root, slug, skillPath);
73
+ }
74
+ catch (err) {
75
+ const message = err instanceof Error ? err.message : "unknown skill validation error";
76
+ errors.push(message);
77
+ }
78
+ return { warnings, errors };
79
+ }
80
+ function maybeLine(label, values) {
81
+ if (values.length === 0) {
82
+ return undefined;
83
+ }
84
+ return `${label}: ${values.join(", ")}`;
85
+ }
86
+ function filterSkills(skills, tags, tagsMode = "any") {
87
+ const normalizedTags = tags?.map((value) => value.toLowerCase()).filter(Boolean) ?? [];
88
+ if (normalizedTags.length === 0) {
89
+ return skills;
90
+ }
91
+ return skills.filter((skill) => {
92
+ const skillTags = new Set(skill.tags.map((value) => value.toLowerCase()));
93
+ if (tagsMode === "all") {
94
+ return normalizedTags.every((value) => skillTags.has(value));
95
+ }
96
+ return normalizedTags.some((value) => skillTags.has(value));
97
+ });
98
+ }
99
+ function buildSkillSearchText(skill) {
100
+ const ochatrTokens = Object.entries(skill.ochatr).flatMap(([key, value]) => {
101
+ if (Array.isArray(value)) {
102
+ return [key, ...value];
103
+ }
104
+ if (typeof value === "boolean") {
105
+ return [key, value ? "true" : "false"];
106
+ }
107
+ return [key, value];
108
+ });
109
+ const tokens = [
110
+ skill.slug,
111
+ skill.id,
112
+ skill.qid,
113
+ skill.name,
114
+ skill.description,
115
+ skill.path,
116
+ ...skill.tags,
117
+ ...skill.authors,
118
+ ...skill.links,
119
+ ...ochatrTokens,
120
+ ];
121
+ return tokens.join(" ").toLowerCase();
122
+ }
123
+ function matchesSkillQuery(skill, terms) {
124
+ const text = buildSkillSearchText(skill);
125
+ for (const term of terms) {
126
+ if (!text.includes(term)) {
127
+ return false;
128
+ }
129
+ }
130
+ return true;
131
+ }
132
+ function runSkillNewCommand(options) {
133
+ const root = options.root;
134
+ const config = (0, config_1.loadConfig)(root);
135
+ const slug = normalizeSlug(options.slug);
136
+ const name = options.name.trim();
137
+ const description = options.description.trim();
138
+ if (!name) {
139
+ throw new errors_1.UsageError("skill name cannot be empty");
140
+ }
141
+ if (!description) {
142
+ throw new errors_1.UsageError("skill description cannot be empty");
143
+ }
144
+ const tags = normalizeLowercaseList(options.tags);
145
+ const authors = parseCsvList(options.authors);
146
+ const links = parseCsvList(options.links);
147
+ const skillsRoot = (0, skills_indexer_1.resolveSkillsRoot)(root, config);
148
+ const skillDir = path_1.default.join(skillsRoot, slug);
149
+ const canonicalPath = path_1.default.join(skillDir, "SKILL.md");
150
+ const compatPath = path_1.default.join(skillDir, "SKILLS.md");
151
+ const force = Boolean(options.force);
152
+ if (fs_1.default.existsSync(compatPath)) {
153
+ throw new errors_1.UsageError(`legacy compatibility file exists for ${slug}; migrate SKILLS.md before scaffolding`);
154
+ }
155
+ if (fs_1.default.existsSync(canonicalPath) && !force) {
156
+ throw new errors_1.UsageError(`skill already exists: ${path_1.default.relative(root, canonicalPath)} (use --force to overwrite)`);
157
+ }
158
+ fs_1.default.mkdirSync(skillDir, { recursive: true });
159
+ fs_1.default.mkdirSync(path_1.default.join(skillDir, "references"), { recursive: true });
160
+ fs_1.default.mkdirSync(path_1.default.join(skillDir, "assets"), { recursive: true });
161
+ if (options.withScripts) {
162
+ fs_1.default.mkdirSync(path_1.default.join(skillDir, "scripts"), { recursive: true });
163
+ }
164
+ const content = (0, skill_support_1.renderSkillTemplate)({
165
+ name,
166
+ description,
167
+ tags,
168
+ authors,
169
+ links,
170
+ });
171
+ fs_1.default.writeFileSync(canonicalPath, content, "utf8");
172
+ (0, skill_support_1.ensureSkillsRegistry)(root, config);
173
+ (0, skill_support_1.refreshSkillsRegistry)(root, config);
174
+ if (config.index.auto_reindex) {
175
+ const skillsIndex = (0, skills_indexer_1.buildSkillsIndex)(root, config);
176
+ (0, skills_index_cache_2.writeSkillsIndex)((0, skills_indexer_1.resolveSkillsIndexPath)(root), skillsIndex);
177
+ }
178
+ (0, event_support_1.appendAutomaticEvent)({
179
+ root,
180
+ ws: "root",
181
+ kind: "SKILL_CREATED",
182
+ status: "ok",
183
+ refs: [`skill:${slug}`],
184
+ notes: `skill created via mdkg skill new`,
185
+ runId: options.runId,
186
+ skill: slug,
187
+ now: options.now,
188
+ });
189
+ console.log(`skill created: root:skill:${slug} (${path_1.default.relative(root, canonicalPath)})`);
190
+ }
191
+ function runSkillListCommand(options) {
192
+ const config = (0, config_1.loadConfig)(options.root);
193
+ const { index, rebuilt, stale } = (0, skills_index_cache_1.loadSkillsIndex)({
194
+ root: options.root,
195
+ config,
196
+ useCache: !options.noCache,
197
+ allowReindex: !options.noReindex,
198
+ });
199
+ if (stale && !rebuilt && !options.noCache) {
200
+ console.error("warning: skills index is stale; run mdkg index to refresh");
201
+ }
202
+ const skills = filterSkills(Object.values(index.skills), options.tags, options.tagsMode ?? "any").sort((a, b) => a.qid.localeCompare(b.qid));
203
+ if (options.json) {
204
+ (0, query_output_1.writeJson)({
205
+ command: "list",
206
+ kind: "skill",
207
+ count: skills.length,
208
+ items: skills.map(query_output_1.toSkillSummaryJson),
209
+ });
210
+ return;
211
+ }
212
+ (0, query_output_1.writeCount)(skills.length, skills.length === 0 ? "no skills matched current filters" : undefined);
213
+ for (const skill of skills) {
214
+ console.log((0, skill_support_1.formatSkillCard)(skill));
215
+ }
216
+ }
217
+ function runSkillShowCommand(options) {
218
+ const config = (0, config_1.loadConfig)(options.root);
219
+ const slug = normalizeSlug(options.slug);
220
+ const { index, rebuilt, stale } = (0, skills_index_cache_1.loadSkillsIndex)({
221
+ root: options.root,
222
+ config,
223
+ useCache: !options.noCache,
224
+ allowReindex: !options.noReindex,
225
+ });
226
+ if (stale && !rebuilt && !options.noCache) {
227
+ console.error("warning: skills index is stale; run mdkg index to refresh");
228
+ }
229
+ const skill = index.skills[slug];
230
+ if (!skill) {
231
+ throw new errors_1.NotFoundError(`skill not found: ${slug}`);
232
+ }
233
+ const skillPath = path_1.default.resolve(options.root, skill.path);
234
+ let body = "";
235
+ if (!options.metaOnly) {
236
+ if (!fs_1.default.existsSync(skillPath)) {
237
+ throw new errors_1.NotFoundError(`file not found for ${skill.id}: ${skill.path}`);
238
+ }
239
+ body = fs_1.default.readFileSync(skillPath, "utf8").trimEnd();
240
+ }
241
+ if (options.json) {
242
+ (0, query_output_1.writeJson)({
243
+ command: "show",
244
+ kind: "skill",
245
+ item: (0, query_output_1.toSkillDetailJson)(skill, options.metaOnly ? undefined : body),
246
+ });
247
+ return;
248
+ }
249
+ if (options.metaOnly) {
250
+ const lines = [];
251
+ lines.push((0, skill_support_1.formatSkillCard)(skill));
252
+ lines.push(`description: ${skill.description}`);
253
+ const tagsLine = maybeLine("tags", skill.tags);
254
+ if (tagsLine) {
255
+ lines.push(tagsLine);
256
+ }
257
+ if (skill.version) {
258
+ lines.push(`version: ${skill.version}`);
259
+ }
260
+ const authorsLine = maybeLine("authors", skill.authors);
261
+ if (authorsLine) {
262
+ lines.push(authorsLine);
263
+ }
264
+ const linksLine = maybeLine("links", skill.links);
265
+ if (linksLine) {
266
+ lines.push(linksLine);
267
+ }
268
+ lines.push(`has_scripts: ${skill.has_scripts ? "true" : "false"}`);
269
+ lines.push(`has_references: ${skill.has_references ? "true" : "false"}`);
270
+ for (const [key, value] of Object.entries(skill.ochatr).sort(([a], [b]) => a.localeCompare(b))) {
271
+ if (Array.isArray(value)) {
272
+ lines.push(`${key}: ${value.join(", ")}`);
273
+ continue;
274
+ }
275
+ if (typeof value === "boolean") {
276
+ lines.push(`${key}: ${value ? "true" : "false"}`);
277
+ continue;
278
+ }
279
+ lines.push(`${key}: ${value}`);
280
+ }
281
+ console.log(lines.join("\n"));
282
+ return;
283
+ }
284
+ console.log(body);
285
+ }
286
+ function runSkillSearchCommand(options) {
287
+ const query = options.query.trim();
288
+ if (!query) {
289
+ throw new errors_1.UsageError("search query cannot be empty");
290
+ }
291
+ const config = (0, config_1.loadConfig)(options.root);
292
+ const { index, rebuilt, stale } = (0, skills_index_cache_1.loadSkillsIndex)({
293
+ root: options.root,
294
+ config,
295
+ useCache: !options.noCache,
296
+ allowReindex: !options.noReindex,
297
+ });
298
+ if (stale && !rebuilt && !options.noCache) {
299
+ console.error("warning: skills index is stale; run mdkg index to refresh");
300
+ }
301
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
302
+ const skills = filterSkills(Object.values(index.skills), options.tags, options.tagsMode ?? "any")
303
+ .filter((skill) => matchesSkillQuery(skill, terms))
304
+ .sort((a, b) => a.qid.localeCompare(b.qid));
305
+ if (options.json) {
306
+ (0, query_output_1.writeJson)({
307
+ command: "search",
308
+ kind: "skill",
309
+ count: skills.length,
310
+ items: skills.map(query_output_1.toSkillSummaryJson),
311
+ });
312
+ return;
313
+ }
314
+ (0, query_output_1.writeCount)(skills.length, skills.length === 0 ? `no skills matched query "${query}"` : undefined);
315
+ for (const skill of skills) {
316
+ console.log((0, skill_support_1.formatSkillCard)(skill));
317
+ }
318
+ }
319
+ function runSkillValidateCommand(options) {
320
+ const config = (0, config_1.loadConfig)(options.root);
321
+ const targetSlug = options.slug?.trim().toLowerCase();
322
+ const warnings = [];
323
+ const errors = [];
324
+ if (targetSlug) {
325
+ const result = validateSingleSkill(options.root, normalizeSlug(targetSlug));
326
+ warnings.push(...result.warnings);
327
+ errors.push(...result.errors);
328
+ }
329
+ else {
330
+ const skillsRoot = (0, skills_indexer_1.resolveSkillsRoot)(options.root, config);
331
+ if (fs_1.default.existsSync(skillsRoot)) {
332
+ const entries = fs_1.default.readdirSync(skillsRoot, { withFileTypes: true });
333
+ for (const entry of entries.filter((value) => value.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
334
+ const result = validateSingleSkill(options.root, entry.name.toLowerCase());
335
+ warnings.push(...result.warnings);
336
+ errors.push(...result.errors);
337
+ }
338
+ }
339
+ }
340
+ for (const warning of Array.from(new Set(warnings))) {
341
+ console.error(`warning: ${warning}`);
342
+ }
343
+ if (errors.length > 0) {
344
+ for (const error of Array.from(new Set(errors))) {
345
+ console.error(error);
346
+ }
347
+ throw new errors_1.ValidationError(`skill validation failed with ${Array.from(new Set(errors)).length} error(s)`);
348
+ }
349
+ if (targetSlug) {
350
+ console.log(`skill validation ok: ${targetSlug} (1 skill checked)`);
351
+ return;
352
+ }
353
+ const checkedCount = fs_1.default.existsSync((0, skills_indexer_1.resolveSkillsRoot)(options.root, config))
354
+ ? fs_1.default
355
+ .readdirSync((0, skills_indexer_1.resolveSkillsRoot)(options.root, config), { withFileTypes: true })
356
+ .filter((value) => value.isDirectory()).length
357
+ : 0;
358
+ console.log(`skill validation ok: ${checkedCount} skill${checkedCount === 1 ? "" : "s"} checked`);
359
+ }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SKILL_REGISTRY_END = exports.SKILL_REGISTRY_START = void 0;
7
+ exports.resolveSkillTemplatePath = resolveSkillTemplatePath;
8
+ exports.loadSkillTemplate = loadSkillTemplate;
9
+ exports.renderSkillTemplate = renderSkillTemplate;
10
+ exports.registryTemplate = registryTemplate;
11
+ exports.ensureSkillsRegistry = ensureSkillsRegistry;
12
+ exports.refreshSkillsRegistry = refreshSkillsRegistry;
13
+ exports.formatSkillCard = formatSkillCard;
14
+ const fs_1 = __importDefault(require("fs"));
15
+ const path_1 = __importDefault(require("path"));
16
+ const skills_indexer_1 = require("../graph/skills_indexer");
17
+ exports.SKILL_REGISTRY_START = "<!-- mdkg:skill-registry:start -->";
18
+ exports.SKILL_REGISTRY_END = "<!-- mdkg:skill-registry:end -->";
19
+ function renderYamlList(values) {
20
+ return `[${values.join(", ")}]`;
21
+ }
22
+ function renderOptionalLine(key, values) {
23
+ if (values.length === 0) {
24
+ return "";
25
+ }
26
+ return `${key}: ${renderYamlList(values)}\n`;
27
+ }
28
+ function resolveSkillTemplatePath() {
29
+ return path_1.default.resolve(__dirname, "..", "init", "skills", "SKILL.md.example");
30
+ }
31
+ function loadSkillTemplate() {
32
+ const templatePath = resolveSkillTemplatePath();
33
+ if (!fs_1.default.existsSync(templatePath)) {
34
+ throw new Error(`missing skill template artifact: ${templatePath}`);
35
+ }
36
+ return fs_1.default.readFileSync(templatePath, "utf8");
37
+ }
38
+ function renderSkillTemplate(data) {
39
+ const template = loadSkillTemplate();
40
+ return template
41
+ .replace("{{name}}", data.name)
42
+ .replace("{{description}}", data.description)
43
+ .replace("{{tags_block}}", renderOptionalLine("tags", data.tags))
44
+ .replace("{{authors_block}}", renderOptionalLine("authors", data.authors))
45
+ .replace("{{links_block}}", renderOptionalLine("links", data.links));
46
+ }
47
+ function registryTemplate() {
48
+ return [
49
+ "# Skills Registry",
50
+ "",
51
+ "This directory stores Agent Skills packages used by mdkg tooling and orchestrators.",
52
+ "",
53
+ "Use `mdkg skill new <slug> \"<name>\" --description \"...\"` to scaffold a new skill from the built-in Anthropic-aligned template.",
54
+ "Use `CLI_COMMAND_MATRIX.md` as the canonical command and flag reference when updating skill procedures.",
55
+ "",
56
+ "## Conventions",
57
+ "",
58
+ "- One folder per skill slug.",
59
+ "- Use `SKILL.md` as the canonical skill entrypoint.",
60
+ "- Keep procedures deterministic and avoid embedding secrets.",
61
+ "- Create `scripts/` only when deterministic execution cannot be expressed safely as instructions.",
62
+ "",
63
+ "## Registered Skills",
64
+ "",
65
+ `${exports.SKILL_REGISTRY_START}`,
66
+ "_No skills registered yet. Run `mdkg skill new` to add one._",
67
+ `${exports.SKILL_REGISTRY_END}`,
68
+ "",
69
+ ].join("\n");
70
+ }
71
+ function renderRegistryLines(skillsIndex) {
72
+ const skills = Object.values(skillsIndex.skills).sort((a, b) => a.slug.localeCompare(b.slug));
73
+ if (skills.length === 0) {
74
+ return ["_No skills registered yet. Run `mdkg skill new` to add one._"];
75
+ }
76
+ const lines = [];
77
+ for (const skill of skills) {
78
+ lines.push(`- \`${skill.slug}\``);
79
+ lines.push(` - name: \`${skill.name}\``);
80
+ const stageTag = skill.tags.find((tag) => tag.startsWith("stage:"));
81
+ if (stageTag) {
82
+ lines.push(` - stage: \`${stageTag}\``);
83
+ }
84
+ const writerTag = skill.tags.find((tag) => tag.startsWith("writer:"));
85
+ if (writerTag) {
86
+ lines.push(` - writer role: \`${writerTag}\``);
87
+ }
88
+ lines.push(` - description: ${skill.description}`);
89
+ }
90
+ return lines;
91
+ }
92
+ function replaceManagedSection(raw, lines) {
93
+ const managed = `${exports.SKILL_REGISTRY_START}\n${lines.join("\n")}\n${exports.SKILL_REGISTRY_END}`;
94
+ if (raw.includes(exports.SKILL_REGISTRY_START) && raw.includes(exports.SKILL_REGISTRY_END)) {
95
+ const pattern = new RegExp(`${exports.SKILL_REGISTRY_START}[\\s\\S]*?${exports.SKILL_REGISTRY_END}`, "m");
96
+ const replaced = raw.replace(pattern, managed);
97
+ return replaced.endsWith("\n") ? replaced : `${replaced}\n`;
98
+ }
99
+ const trimmed = raw.trimEnd();
100
+ const prefix = trimmed.length > 0 ? `${trimmed}\n\n## Registered Skills\n\n` : "## Registered Skills\n\n";
101
+ return `${prefix}${managed}\n`;
102
+ }
103
+ function ensureSkillsRegistry(root, config) {
104
+ const skillsRoot = (0, skills_indexer_1.resolveSkillsRoot)(root, config);
105
+ const registryPath = path_1.default.join(skillsRoot, "registry.md");
106
+ if (!fs_1.default.existsSync(registryPath)) {
107
+ fs_1.default.mkdirSync(path_1.default.dirname(registryPath), { recursive: true });
108
+ fs_1.default.writeFileSync(registryPath, registryTemplate(), "utf8");
109
+ }
110
+ return registryPath;
111
+ }
112
+ function refreshSkillsRegistry(root, config) {
113
+ const registryPath = ensureSkillsRegistry(root, config);
114
+ const raw = fs_1.default.readFileSync(registryPath, "utf8");
115
+ const index = (0, skills_indexer_1.buildSkillsIndex)(root, config);
116
+ const updated = replaceManagedSection(raw, renderRegistryLines(index));
117
+ fs_1.default.writeFileSync(registryPath, updated, "utf8");
118
+ }
119
+ function formatSkillCard(skill) {
120
+ return [skill.qid, "skill", "-/-", skill.name, skill.path].join(" | ");
121
+ }