From 3530a5740b0d0a413190b0529a9710ba03d3eb3a Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 30 Mar 2026 13:21:30 -0700 Subject: [PATCH] Prefix all cross-references when renaming skills, not just frontmatter Mirrors the build pipeline's prefixSkillReferences logic: when prefixing skills after install, now also replaces /command-name invocations and "the command-name skill" prose references throughout each SKILL.md body. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/commands/skills.mjs | 46 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/bin/commands/skills.mjs b/bin/commands/skills.mjs index 369ac4e1f..f2f154e35 100644 --- a/bin/commands/skills.mjs +++ b/bin/commands/skills.mjs @@ -75,7 +75,45 @@ function isAlreadyInstalled(root) { return null; } +function escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function prefixSkillContent(content, prefix, allSkillNames) { + // Prefix the name in frontmatter + let result = content.replace(/^name:\s*(.+)$/m, (_, name) => `name: ${prefix}${name.trim()}`); + + // Prefix cross-references: /skillname -> /prefix-skillname + const sorted = [...allSkillNames].sort((a, b) => b.length - a.length); + for (const name of sorted) { + // Command invocations: /skillname + result = result.replace( + new RegExp(`/(?=${escapeRegex(name)}(?:[^a-zA-Z0-9_-]|$))`, 'g'), + `/${prefix}` + ); + // Prose references: "the skillname skill" + result = result.replace( + new RegExp(`(the) ${escapeRegex(name)} skill`, 'gi'), + (_, article) => `${article} ${prefix}${name} skill` + ); + } + return result; +} + function renameSkillsWithPrefix(root, prefix) { + // First pass: collect all skill names across all providers (use first provider found) + let allSkillNames = []; + for (const d of PROVIDER_DIRS) { + const skillsDir = join(root, d, 'skills'); + if (!existsSync(skillsDir)) continue; + const entries = readdirSync(skillsDir, { withFileTypes: true }); + allSkillNames = entries + .filter(e => e.isDirectory() && existsSync(join(skillsDir, e.name, 'SKILL.md'))) + .map(e => e.name); + if (allSkillNames.length > 0) break; + } + + // Second pass: rename and prefix content let count = 0; for (const d of PROVIDER_DIRS) { const skillsDir = join(root, d, 'skills'); @@ -84,20 +122,18 @@ function renameSkillsWithPrefix(root, prefix) { const entries = readdirSync(skillsDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; - // Skip if already prefixed or not an impeccable skill const skillMd = join(skillsDir, entry.name, 'SKILL.md'); if (!existsSync(skillMd)) continue; if (entry.name.startsWith(prefix)) continue; - const newName = prefix + entry.name; const src = join(skillsDir, entry.name); - const dest = join(skillsDir, newName); + const dest = join(skillsDir, prefix + entry.name); renameSync(src, dest); - // Update the name in SKILL.md frontmatter + // Prefix frontmatter name + all cross-references in SKILL.md let content = readFileSync(join(dest, 'SKILL.md'), 'utf8'); - content = content.replace(/^name:\s*(.+)$/m, (_, name) => `name: ${prefix}${name.trim()}`); + content = prefixSkillContent(content, prefix, allSkillNames); writeFileSync(join(dest, 'SKILL.md'), content); count++; }