Skip to content

Instantly share code, notes, and snippets.

@petekp
Last active February 26, 2026 16:38
Show Gist options
  • Select an option

  • Save petekp/78e9188dc1da2b01663e0942fd703dbe to your computer and use it in GitHub Desktop.

Select an option

Save petekp/78e9188dc1da2b01663e0942fd703dbe to your computer and use it in GitHub Desktop.
Skill Auditor — finds duplicate SKILL.md files across all locations and interactively prompts for cleanup
#!/usr/bin/env node
/**
* Skill Audit Tool
*
* Auto-discovers ALL SKILL.md files across your system — Claude Code,
* Codex, Copilot, Cursor, Gemini, OpenCode, Goose, Continue, Factory,
* OpenClaw, OpenHands, .agents, project repos, and more.
*
* Identifies duplicates by name and content similarity,
* and interactively prompts for deletion.
*
* Usage:
* node skill-audit.mjs Interactive mode (prompts for each dupe)
* node skill-audit.mjs --report Report-only mode (no deletions, full overview)
* node skill-audit.mjs --skip-managed Skip cache/marketplace dupes in interactive mode
*/
import { readdir, readFile, stat, readlink, rm, lstat, realpath } from "node:fs/promises";
import { join, resolve, dirname, basename, relative } from "node:path";
import { homedir } from "node:os";
import { createInterface } from "node:readline";
const HOME = homedir();
const args = new Set(process.argv.slice(2));
const REPORT_ONLY = args.has("--report");
const SKIP_MANAGED = args.has("--skip-managed");
// ─── Auto-discovery: build scan roots dynamically ───
async function discoverScanRoots() {
const roots = new Set();
// Known Claude Code paths
const claudeRoots = [
join(HOME, ".claude/skills"),
join(HOME, ".claude/plugins/local"),
join(HOME, ".claude/plugins/cache"),
join(HOME, ".claude/plugins/marketplaces"),
];
// Global .agents directory
const agentsRoots = [
join(HOME, ".agents/skills"),
join(HOME, ".agents"),
];
// Other agent tool config directories that may contain skills
const agentToolDirs = [
".codex/skills",
".config/agents/skills",
".config/crush/skills",
".config/goose/skills",
".config/opencode/skills",
".config/opencode/skill",
".continue/skills",
".copilot/skills",
".cursor/skills-cursor",
".factory/skills",
".gemini/skills",
".gemini/antigravity/skills",
".openclaw/skills",
".openhands/skills",
];
// Known development repos
const devRepos = [
join(HOME, "Code/agent-skills/skills"),
join(HOME, "Code/openclaw/skills"),
];
// Other known locations
const otherRoots = [
join(HOME, ".local/share/shaping-skills"),
];
// Add all known paths
for (const p of [...claudeRoots, ...agentsRoots, ...devRepos, ...otherRoots]) {
roots.add(p);
}
for (const rel of agentToolDirs) {
roots.add(join(HOME, rel));
}
// Auto-discover: scan $HOME for any dotdir containing a "skills" subdirectory
try {
const homeEntries = await readdir(HOME, { withFileTypes: true });
for (const entry of homeEntries) {
if (!entry.name.startsWith(".")) continue;
if (entry.name === ".Trash" || entry.name === ".npm" || entry.name === ".nvm") continue;
const dotdir = join(HOME, entry.name);
const skillsDir = join(dotdir, "skills");
try {
const s = await stat(skillsDir);
if (s.isDirectory()) roots.add(skillsDir);
} catch {}
}
} catch {}
// Auto-discover: scan Code/ for project-level .agents directories
const codeDir = join(HOME, "Code");
try {
const codeEntries = await readdir(codeDir, { withFileTypes: true });
for (const entry of codeEntries) {
if (!entry.isDirectory()) continue;
const projectAgents = join(codeDir, entry.name, ".agents/skills");
try {
const s = await stat(projectAgents);
if (s.isDirectory()) roots.add(projectAgents);
} catch {}
// Also check nested (monorepo) projects one level deeper
try {
const subEntries = await readdir(join(codeDir, entry.name), { withFileTypes: true });
for (const sub of subEntries) {
if (!sub.isDirectory()) continue;
const nested = join(codeDir, entry.name, sub.name, ".agents/skills");
try {
const s = await stat(nested);
if (s.isDirectory()) roots.add(nested);
} catch {}
}
} catch {}
}
} catch {}
// Filter to only roots that actually exist
const validRoots = [];
for (const root of roots) {
try {
await stat(root);
validRoots.push(root);
} catch {}
}
return validRoots;
}
// ─── ANSI colors ───────────────────────────────
const c = {
reset: "\x1b[0m",
bold: "\x1b[1m",
dim: "\x1b[2m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
cyan: "\x1b[36m",
gray: "\x1b[90m",
white: "\x1b[37m",
};
// ─── Location classification ───────────────────
function classifyLocation(filepath) {
const rel = relative(HOME, filepath);
// Claude Code
if (rel.startsWith(".claude/plugins/cache/")) return "claude-cache";
if (rel.startsWith(".claude/plugins/local/")) return "claude-local-plugin";
if (rel.startsWith(".claude/plugins/marketplaces/")) return "claude-marketplace";
if (rel.startsWith(".claude/skills") || rel.startsWith("Code/claude-code-setup/skills"))
return "claude-active";
// Global .agents
if (rel.startsWith(".agents/")) return "agents-global";
// Other agent tools (dotdirs)
if (rel.startsWith(".codex/")) return "codex";
if (rel.startsWith(".config/agents/")) return "config-agents";
if (rel.startsWith(".config/crush/")) return "crush";
if (rel.startsWith(".config/goose/")) return "goose";
if (rel.startsWith(".config/opencode/")) return "opencode";
if (rel.startsWith(".continue/")) return "continue";
if (rel.startsWith(".copilot/")) return "copilot";
if (rel.startsWith(".cursor/")) return "cursor";
if (rel.startsWith(".factory/")) return "factory";
if (rel.startsWith(".gemini/")) return "gemini";
if (rel.startsWith(".openclaw/")) return "openclaw";
if (rel.startsWith(".openhands/")) return "openhands";
// Dev repos
if (rel.startsWith("Code/agent-skills/")) return "agent-skills-repo";
if (rel.startsWith("Code/openclaw/")) return "openclaw-repo";
if (rel.startsWith("Code/")) return "project-repo";
// Other
if (rel.startsWith(".local/share/shaping-skills")) return "shaping";
return "other";
}
function locationLabel(loc) {
const labels = {
"claude-active": `${c.green}CLAUDE ACTIVE${c.reset}`,
"claude-cache": `${c.gray}CLAUDE CACHE${c.reset}`,
"claude-local-plugin": `${c.cyan}CLAUDE LOCAL PLUGIN${c.reset}`,
"claude-marketplace": `${c.blue}CLAUDE MARKETPLACE${c.reset}`,
"agents-global": `${c.yellow}~/.agents${c.reset}`,
codex: `${c.magenta}CODEX${c.reset}`,
"config-agents": `${c.magenta}.config/agents${c.reset}`,
crush: `${c.magenta}CRUSH${c.reset}`,
goose: `${c.magenta}GOOSE${c.reset}`,
opencode: `${c.magenta}OPENCODE${c.reset}`,
continue: `${c.magenta}CONTINUE${c.reset}`,
copilot: `${c.magenta}COPILOT${c.reset}`,
cursor: `${c.magenta}CURSOR${c.reset}`,
factory: `${c.magenta}FACTORY${c.reset}`,
gemini: `${c.magenta}GEMINI${c.reset}`,
openclaw: `${c.magenta}OPENCLAW${c.reset}`,
openhands: `${c.magenta}OPENHANDS${c.reset}`,
"agent-skills-repo": `${c.blue}AGENT-SKILLS REPO${c.reset}`,
"openclaw-repo": `${c.blue}OPENCLAW REPO${c.reset}`,
"project-repo": `${c.blue}PROJECT REPO${c.reset}`,
shaping: `${c.yellow}SHAPING${c.reset}`,
other: `${c.dim}OTHER${c.reset}`,
};
return labels[loc] || `${c.dim}${loc}${c.reset}`;
}
// Priority: lower = more authoritative (keep first)
function locationPriority(loc) {
const priorities = {
"claude-active": 0,
"claude-local-plugin": 1,
"agents-global": 2,
"agent-skills-repo": 3,
"openclaw-repo": 4,
"project-repo": 5,
shaping: 6,
codex: 10,
"config-agents": 10,
copilot: 10,
cursor: 10,
gemini: 10,
opencode: 10,
continue: 10,
goose: 10,
crush: 10,
factory: 10,
openclaw: 10,
openhands: 10,
"claude-marketplace": 15,
"claude-cache": 20,
other: 25,
};
return priorities[loc] ?? 99;
}
function isManaged(location) {
return location === "claude-cache" || location === "claude-marketplace";
}
function isAgentToolCopy(location) {
return locationPriority(location) === 10;
}
// ─── File discovery ────────────────────────────
async function findSkillFiles(root) {
const results = [];
async function walk(dir, depth = 0) {
if (depth > 8) return;
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const full = join(dir, entry.name);
if (entry.name === "node_modules" || entry.name === ".git") continue;
if (entry.isDirectory() || entry.isSymbolicLink()) {
const skillPath = join(full, "SKILL.md");
try {
await stat(skillPath);
results.push(skillPath);
} catch {}
try {
await walk(full, depth + 1);
} catch {}
}
}
}
try {
await stat(root);
await walk(root);
} catch {}
return results;
}
// ─── Content analysis ──────────────────────────
function extractFrontmatter(content) {
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return {};
const fm = {};
for (const line of match[1].split("\n")) {
const kv = line.match(/^(\w[\w-]*):\s*(.+)/);
if (kv) fm[kv[1]] = kv[2].replace(/^["']|["']$/g, "");
}
return fm;
}
function normalizeContent(content) {
return content
.replace(/^---[\s\S]*?---\n?/, "")
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
}
function trigramSimilarity(a, b) {
if (!a || !b) return 0;
const sa = a.slice(0, 2000);
const sb = b.slice(0, 2000);
const trigramsA = new Set();
const trigramsB = new Set();
for (let i = 0; i <= sa.length - 3; i++) trigramsA.add(sa.slice(i, i + 3));
for (let i = 0; i <= sb.length - 3; i++) trigramsB.add(sb.slice(i, i + 3));
let intersection = 0;
for (const t of trigramsA) if (trigramsB.has(t)) intersection++;
const union = trigramsA.size + trigramsB.size - intersection;
return union === 0 ? 0 : intersection / union;
}
async function resolveSymlink(filepath) {
try {
const s = await lstat(filepath);
if (s.isSymbolicLink()) {
const target = await readlink(filepath);
return resolve(dirname(filepath), target);
}
} catch {}
return null;
}
function shortPath(p) {
return relative(HOME, p).replace(/\/SKILL\.md$/, "");
}
function sortByPriority(group) {
return [...group].sort((a, b) => {
const diff = locationPriority(a.location) - locationPriority(b.location);
if (diff !== 0) return diff;
return b.size - a.size;
});
}
// ─── Main ──────────────────────────────────────
async function main() {
console.log(`\n${c.bold}🔍 Skill Audit Tool${c.reset}`);
if (REPORT_ONLY) console.log(`${c.dim}Mode: report only (no deletions)${c.reset}`);
if (SKIP_MANAGED) console.log(`${c.dim}Mode: skipping cache/marketplace dupes${c.reset}`);
console.log(`${c.dim}Auto-discovering skill locations...${c.reset}\n`);
// 1. Discover scan roots
const scanRoots = await discoverScanRoots();
console.log(`${c.bold}Scanning ${c.cyan}${scanRoots.length}${c.reset}${c.bold} locations:${c.reset}`);
for (const root of scanRoots.sort()) {
console.log(` ${c.dim}${relative(HOME, root)}${c.reset}`);
}
console.log();
// 2. Find all SKILL.md files
const pathSet = new Set();
for (const root of scanRoots) {
const found = await findSkillFiles(root);
for (const f of found) {
// Deduplicate by real path to avoid counting symlinks to the same file
try {
const real = await realpath(f);
if (!pathSet.has(real)) {
pathSet.add(real);
}
} catch {
pathSet.add(f);
}
}
}
// But we want to keep all paths (including symlink origins) for display purposes
// Re-scan but track which real paths we've already loaded content for
const allPaths = [];
for (const root of scanRoots) {
const found = await findSkillFiles(root);
allPaths.push(...found);
}
const skills = [];
for (const p of allPaths) {
let real;
try {
real = await realpath(p);
} catch {
real = p;
}
// Allow same real path to appear if from different logical locations
// (e.g., a symlink in .claude/skills and the actual file in .agents)
const dir = dirname(p);
const skillName = basename(dir);
const location = classifyLocation(p);
let content = "";
try {
content = await readFile(p, "utf-8");
} catch {
continue;
}
const frontmatter = extractFrontmatter(content);
const normalized = normalizeContent(content);
const symTarget = await resolveSymlink(dir);
skills.push({
path: p,
realPath: real,
dir,
skillName,
fmName: frontmatter.name || skillName,
description: (frontmatter.description || "").slice(0, 120),
location,
normalized,
symTarget,
size: content.length,
});
}
// Deduplicate entries with identical real paths AND identical locations
const uniqueSkills = [];
const seen = new Set();
for (const s of skills) {
const key = `${s.realPath}::${s.location}`;
if (!seen.has(key)) {
seen.add(key);
uniqueSkills.push(s);
}
}
console.log(
`${c.bold}Found ${c.cyan}${uniqueSkills.length}${c.reset}${c.bold} SKILL.md files${c.reset}\n`
);
// Location summary
const locationCounts = {};
for (const s of uniqueSkills) {
locationCounts[s.location] = (locationCounts[s.location] || 0) + 1;
}
console.log(`${c.bold}Skills by location:${c.reset}`);
for (const [loc, count] of Object.entries(locationCounts).sort((a, b) => b[1] - a[1])) {
console.log(` ${locationLabel(loc)} ${c.bold}${count}${c.reset}`);
}
console.log();
// 3. Group by name
const byName = new Map();
for (const s of uniqueSkills) {
const key = s.fmName || s.skillName;
if (!byName.has(key)) byName.set(key, []);
byName.get(key).push(s);
}
// 4. Name-based duplicates
const nameDupes = [];
for (const [name, group] of byName) {
if (group.length > 1) nameDupes.push({ name, skills: group });
}
// Categorize duplicate groups
const actionableDupes = []; // has non-managed, non-agent-tool copies
const agentToolDupes = []; // dupes spread across agent tools
const managedOnlyDupes = []; // only claude cache/marketplace
for (const dupe of nameDupes) {
const sorted = sortByPriority(dupe.skills);
const extras = sorted.slice(1);
const hasAgentTool = extras.some((s) => isAgentToolCopy(s.location));
const hasSource = extras.some((s) => !isManaged(s.location) && !isAgentToolCopy(s.location));
if (hasSource) {
actionableDupes.push({ ...dupe, sorted });
} else if (hasAgentTool) {
agentToolDupes.push({ ...dupe, sorted });
} else {
managedOnlyDupes.push({ ...dupe, sorted });
}
}
// 5. Content-similar skills (different names)
const contentDupes = [];
const checked = new Set();
const uniqueNames = [...byName.keys()];
for (let i = 0; i < uniqueNames.length; i++) {
for (let j = i + 1; j < uniqueNames.length; j++) {
const nameA = uniqueNames[i];
const nameB = uniqueNames[j];
const a = byName.get(nameA)[0];
const b = byName.get(nameB)[0];
const sim = trigramSimilarity(a.normalized, b.normalized);
if (sim > 0.65) {
const key = [nameA, nameB].sort().join("|");
if (!checked.has(key)) {
checked.add(key);
contentDupes.push({ nameA, nameB, similarity: sim, skillA: a, skillB: b });
}
}
}
}
// ─── Report Mode ─────────────────────────────
if (REPORT_ONLY) {
printReportMode(actionableDupes, agentToolDupes, managedOnlyDupes, contentDupes, byName, uniqueSkills);
return;
}
// ─── Interactive Mode ────────────────────────
const totalDupeGroups = actionableDupes.length + agentToolDupes.length + managedOnlyDupes.length;
if (totalDupeGroups === 0 && contentDupes.length === 0) {
console.log(`${c.green}✓ No duplicates found!${c.reset}`);
return;
}
const rl = createInterface({ input: process.stdin, output: process.stdout });
const ask = (q) => new Promise((res) => rl.question(q, res));
let deleted = 0;
let skipped = 0;
let quit = false;
// Phase 1: Actionable dupes (source-level conflicts)
if (actionableDupes.length > 0) {
console.log(
`${c.bold}${c.red}═══ Source-Level Duplicates (${actionableDupes.length} groups) ═══${c.reset}`
);
console.log(
`${c.dim}Same skill exists in multiple source/development locations${c.reset}\n`
);
for (const { name, sorted } of actionableDupes.sort((a, b) => b.sorted.length - a.sorted.length)) {
if (quit) break;
({ deleted, skipped, quit } = await handleDupeGroup(name, sorted, ask, deleted, skipped, quit));
}
}
// Phase 2: Agent tool spread (same skill copied to codex, copilot, gemini, etc.)
if (!quit && agentToolDupes.length > 0) {
console.log(
`\n${c.bold}${c.magenta}═══ Agent Tool Copies (${agentToolDupes.length} groups) ═══${c.reset}`
);
console.log(
`${c.dim}Skills copied across Codex, Copilot, Gemini, OpenCode, etc.${c.reset}\n`
);
const answer = await ask(
` ${c.yellow}Review agent-tool copies? [y/N/q] ${c.reset}`
);
if (answer.toLowerCase() === "q") {
quit = true;
} else if (answer.toLowerCase() === "y") {
for (const { name, sorted } of agentToolDupes.sort((a, b) => b.sorted.length - a.sorted.length)) {
if (quit) break;
({ deleted, skipped, quit } = await handleDupeGroup(name, sorted, ask, deleted, skipped, quit));
}
} else {
const total = agentToolDupes.reduce((sum, d) => sum + d.sorted.length - 1, 0);
skipped += total;
console.log(` ${c.dim}Skipped ${total} agent-tool copies${c.reset}\n`);
}
}
// Phase 3: Managed (Claude cache/marketplace)
if (!quit && managedOnlyDupes.length > 0 && !SKIP_MANAGED) {
console.log(
`\n${c.bold}${c.gray}═══ Claude Cache/Marketplace (${managedOnlyDupes.length} groups) ═══${c.reset}`
);
console.log(
`${c.dim}Plugin system managed copies (usually safe to skip)${c.reset}\n`
);
const answer = await ask(
` ${c.yellow}Review managed copies? [y/N/q] ${c.reset}`
);
if (answer.toLowerCase() === "q") {
quit = true;
} else if (answer.toLowerCase() !== "y") {
const total = managedOnlyDupes.reduce((sum, d) => sum + d.sorted.length - 1, 0);
skipped += total;
console.log(` ${c.dim}Skipped ${total} managed copies${c.reset}\n`);
} else {
for (const { name, sorted } of managedOnlyDupes.sort((a, b) => b.sorted.length - a.sorted.length)) {
if (quit) break;
console.log(`\n${c.bold}${c.cyan}● "${name}"${c.reset} — ${sorted.length} copies`);
for (let i = 0; i < sorted.length; i++) {
const s = sorted[i];
const marker = i === 0 ? `${c.green} ★ KEEP${c.reset}` : "";
console.log(
` ${i + 1}) [${locationLabel(s.location)}] ${c.dim}${shortPath(s.path)}${c.reset}${marker}`
);
}
const answer = await ask(` Delete all copies except #1? [y/N/s/q] `);
if (answer.toLowerCase() === "q") { quit = true; break; }
if (answer.toLowerCase() === "y") {
for (let i = 1; i < sorted.length; i++) {
try {
await rm(sorted[i].dir, { recursive: true });
console.log(` ${c.red}✗ Deleted${c.reset} ${shortPath(sorted[i].path)}`);
deleted++;
} catch (err) {
console.log(` ${c.red}Error:${c.reset} ${err.message}`);
}
}
} else {
skipped += sorted.length - 1;
}
}
}
}
// Phase 4: Content-similar pairs
if (!quit && contentDupes.length > 0) {
console.log(
`\n${c.bold}${c.cyan}═══ Content-Similar Skills (${contentDupes.length} pairs) ═══${c.reset}\n`
);
for (const { nameA, nameB, similarity, skillA, skillB } of contentDupes.sort(
(a, b) => b.similarity - a.similarity
)) {
if (quit) break;
const pct = (similarity * 100).toFixed(0);
console.log(
`${c.bold}●${c.reset} ${c.bold}"${nameA}"${c.reset} ↔ ${c.bold}"${nameB}"${c.reset} ${c.yellow}${pct}% similar${c.reset}`
);
console.log(
` A: [${locationLabel(skillA.location)}] ${c.dim}${shortPath(skillA.path)}${c.reset} (${(skillA.size / 1024).toFixed(1)}KB)`
);
console.log(
` B: [${locationLabel(skillB.location)}] ${c.dim}${shortPath(skillB.path)}${c.reset} (${(skillB.size / 1024).toFixed(1)}KB)`
);
if (skillA.description) console.log(` ${c.dim}A: ${skillA.description}${c.reset}`);
if (skillB.description) console.log(` ${c.dim}B: ${skillB.description}${c.reset}`);
const answer = await ask(`\n Delete one? [a/b/N/q] `);
if (answer.toLowerCase() === "q") { quit = true; break; }
const target = answer.toLowerCase() === "a" ? skillA : answer.toLowerCase() === "b" ? skillB : null;
if (target) {
try {
await rm(target.dir, { recursive: true });
console.log(` ${c.red}✗ Deleted${c.reset} ${shortPath(target.path)}`);
deleted++;
} catch (err) {
console.log(` ${c.red}Error:${c.reset} ${err.message}`);
}
} else {
skipped++;
}
console.log();
}
}
rl.close();
printSummary(deleted, skipped);
}
// ─── Shared interactive handler for a dupe group ──
async function handleDupeGroup(name, sorted, ask, deleted, skipped, quit) {
console.log(`${c.bold}${c.cyan}● "${name}"${c.reset} — ${sorted.length} copies\n`);
for (let i = 0; i < sorted.length; i++) {
const s = sorted[i];
const loc = locationLabel(s.location);
const sym = s.symTarget ? ` ${c.dim}→ ${relative(HOME, s.symTarget)}${c.reset}` : "";
const sizeKb = (s.size / 1024).toFixed(1);
const marker = i === 0 ? `${c.green} ★ KEEP${c.reset}` : "";
console.log(
` ${c.bold}${i + 1})${c.reset} [${loc}] ${c.dim}${shortPath(s.path)}${c.reset} (${sizeKb}KB)${sym}${marker}`
);
if (s.description) console.log(` ${c.dim}${s.description}${c.reset}`);
}
console.log();
for (let i = 1; i < sorted.length; i++) {
if (quit) break;
const s = sorted[i];
if (SKIP_MANAGED && isManaged(s.location)) {
skipped++;
continue;
}
const contentMatch = trigramSimilarity(sorted[0].normalized, s.normalized);
const matchLabel =
contentMatch > 0.95
? `${c.green}IDENTICAL${c.reset}`
: contentMatch > 0.7
? `${c.yellow}SIMILAR ${(contentMatch * 100).toFixed(0)}%${c.reset}`
: `${c.red}DIFFERENT ${(contentMatch * 100).toFixed(0)}%${c.reset}`;
const answer = await ask(
` Delete ${c.bold}${shortPath(s.path)}${c.reset} [${locationLabel(s.location)}] [${matchLabel}]? [y/N/s(kip group)/q] `
);
if (answer.toLowerCase() === "q") {
quit = true;
break;
}
if (answer.toLowerCase() === "s") {
skipped += sorted.length - i;
console.log(` ${c.dim}Skipped rest of group${c.reset}`);
break;
}
if (answer.toLowerCase() === "y") {
try {
await rm(s.dir, { recursive: true });
console.log(` ${c.red}✗ Deleted${c.reset} ${shortPath(s.path)}`);
deleted++;
} catch (err) {
console.log(` ${c.red}Error:${c.reset} ${err.message}`);
}
} else {
skipped++;
}
}
console.log();
return { deleted, skipped, quit };
}
// ─── Report mode output ───────────────────────
function printReportMode(actionableDupes, agentToolDupes, managedOnlyDupes, contentDupes, byName, uniqueSkills) {
if (actionableDupes.length > 0) {
console.log(
`${c.bold}${c.red}═══ Source-Level Duplicates (${actionableDupes.length} groups) ═══${c.reset}`
);
console.log(
`${c.dim}Same skill in multiple source/development locations${c.reset}\n`
);
for (const { name, sorted } of actionableDupes.sort((a, b) => b.sorted.length - a.sorted.length)) {
console.log(`${c.bold}${c.cyan}● "${name}"${c.reset} — ${sorted.length} copies`);
for (let i = 0; i < sorted.length; i++) {
const s = sorted[i];
const sym = s.symTarget ? ` ${c.dim}→ ${relative(HOME, s.symTarget)}${c.reset}` : "";
const marker = i === 0 ? `${c.green} ★ KEEP${c.reset}` : "";
console.log(
` ${i + 1}) [${locationLabel(s.location)}] ${c.dim}${shortPath(s.path)}${c.reset}${sym}${marker}`
);
}
console.log();
}
}
if (agentToolDupes.length > 0) {
console.log(
`${c.bold}${c.magenta}═══ Agent Tool Copies (${agentToolDupes.length} groups) ═══${c.reset}`
);
console.log(
`${c.dim}Skills spread across Codex, Copilot, Gemini, OpenCode, etc.${c.reset}\n`
);
for (const { name, sorted } of agentToolDupes.sort((a, b) => b.sorted.length - a.sorted.length)) {
const locs = [...new Set(sorted.map((s) => s.location))].map(locationLabel).join(", ");
console.log(` ${c.dim}${name}${c.reset} (${sorted.length} copies: ${locs})`);
}
console.log();
}
if (managedOnlyDupes.length > 0) {
console.log(
`${c.bold}${c.gray}═══ Claude Cache/Marketplace (${managedOnlyDupes.length} groups) ═══${c.reset}`
);
console.log(
`${c.dim}Plugin system managed copies${c.reset}\n`
);
for (const { name, sorted } of managedOnlyDupes.sort((a, b) => b.sorted.length - a.sorted.length)) {
const locs = [...new Set(sorted.map((s) => s.location))].map(locationLabel).join(", ");
console.log(` ${c.dim}${name}${c.reset} (${sorted.length} copies: ${locs})`);
}
console.log();
}
if (contentDupes.length > 0) {
console.log(
`${c.bold}${c.cyan}═══ Content-Similar Skills (${contentDupes.length} pairs) ═══${c.reset}\n`
);
for (const { nameA, nameB, similarity, skillA, skillB } of contentDupes.sort(
(a, b) => b.similarity - a.similarity
)) {
const pct = (similarity * 100).toFixed(0);
console.log(
` ${c.bold}"${nameA}"${c.reset} ↔ ${c.bold}"${nameB}"${c.reset} ${c.yellow}${pct}%${c.reset}`
);
console.log(
` [${locationLabel(skillA.location)}] ${c.dim}${shortPath(skillA.path)}${c.reset}`
);
console.log(
` [${locationLabel(skillB.location)}] ${c.dim}${shortPath(skillB.path)}${c.reset}`
);
}
console.log();
}
const uniqueCount = byName.size;
const dupeCount = uniqueSkills.length - uniqueCount;
const agentToolTotal = agentToolDupes.reduce((sum, d) => sum + d.sorted.length - 1, 0);
console.log(`${c.bold}═══ Totals ═══${c.reset}`);
console.log(` Unique skill names: ${c.bold}${uniqueCount}${c.reset}`);
console.log(` Total copies on disk: ${c.bold}${uniqueSkills.length}${c.reset}`);
console.log(` Duplicate copies: ${c.bold}${dupeCount}${c.reset}`);
console.log(` Source-level conflicts: ${c.bold}${c.red}${actionableDupes.length}${c.reset}`);
console.log(` Agent tool copies: ${c.bold}${c.magenta}${agentToolTotal}${c.reset} (across ${agentToolDupes.length} skills)`);
console.log(` Claude managed dupes: ${c.bold}${managedOnlyDupes.length}${c.reset}`);
console.log(` Content-similar pairs: ${c.bold}${c.cyan}${contentDupes.length}${c.reset}`);
console.log();
}
function printSummary(deleted, skipped) {
console.log(`\n${c.bold}═══ Summary ═══${c.reset}`);
console.log(` ${c.red}Deleted:${c.reset} ${deleted}`);
console.log(` ${c.dim}Skipped:${c.reset} ${skipped}`);
console.log();
}
main().catch((err) => {
console.error(`${c.red}Fatal error:${c.reset}`, err);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment