Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "skills-atlas",
"version": "0.30.0",
"version": "0.40.0",
"description": "Find, read, install and update public Agent Skills from a catalog of about 75,000: a skill, MCP tools and a /skills-atlas command.",
"author": {
"name": "DevOhMyCode",
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

## 0.40.0 — 2026-11-10

Validation: 286 tests and TypeScript checks.

- **`skills-atlas unused`** lists the installed skills Claude Code has not used in the last 30 days, read from its session transcripts on this machine, and `--remove` uninstalls them. Skills installed during the window and skills of other agents are listed apart, never as unused. Also as the MCP tool `unused_skills`.
- **`skills-atlas overlaps`** lists the installed skills whose descriptions are close enough for an agent to take one for the other, with a score and the words they share. Also as the MCP tool `overlapping_skills`.
- **A source that fails no longer drops its skills.** The crawl keeps what that source listed in the previous snapshot. The snapshot of 2026-11-10, built while claude-plugins.dev answered HTTP 402, had lost 35,750 skills; the one of 2026-11-04 is restored.
- **Content read from a URL is a warning sign.** A line where the skill fetches, reads or downloads something at a URL is reported (medium): what is served there can change after the review, and pinning the skill to a commit does not pin it.
- **Secret keys written in a skill are reported** (medium), cut to their first characters; placeholders such as `sk-xxxxxxxx` are not.
Expand Down
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ Copies do not count. A repository that shares at least three growing skill names
Three ways to let an agent find and install skills for you, from the lightest:

- **The skill.** `npx skills add devohmycode/skills-atlas -s skills-atlas` teaches any agent the commands above (`list --json`, `show`, `install … -y`, `outdated --json`). Ask "find me a skill to draw diagrams": the agent searches, reads the candidates' `SKILL.md`, then installs the one you pick.
- **The MCP server.** `skills-atlas mcp` serves the same catalog as tools: `search_skills`, `list_themes`, `show_skill` (with its risk check), `list_packs`, `list_installed`, `outdated_skills` (`{ skills, unchecked }`), `install_skills` (with `dry_run`; it refuses to replace a skill of the same name unless `replace: true`) and `remove_skills`. Installs and removals run one at a time per server, each re-checking what is installed; reads and dry runs do not wait. In Claude Code:
- **The MCP server.** `skills-atlas mcp` serves the same catalog as tools: `search_skills`, `list_themes`, `show_skill` (with its risk check), `list_packs`, `list_installed`, `unused_skills` (what Claude Code has not used lately), `overlapping_skills` (descriptions an agent may confuse), `outdated_skills` (`{ skills, unchecked }`), `install_skills` (with `dry_run`; it refuses to replace a skill of the same name unless `replace: true`) and `remove_skills`. Installs and removals run one at a time per server, each re-checking what is installed; reads and dry runs do not wait. In Claude Code:

```bash
claude mcp add skills-atlas -- npx -y skills-atlas@latest mcp
Expand Down Expand Up @@ -268,6 +268,28 @@ Only `repository` (a GitHub repository URL) and `reason` are required. Without `

*Agents step: every agent the skills CLI supports, with those detected on this machine flagged `· detected`.*

### Unused skills

Every installed skill costs its name and description in each session, used or not. `skills-atlas unused` reads Claude Code's session transcripts on this machine (`~/.claude/projects`, or `CLAUDE_CONFIG_DIR`) and lists the installed skills it has not used in the last 30 days (`--days <n>`): a skill counts as used when the agent called it with the Skill tool, when you typed it as a slash command, or when the agent read its `SKILL.md`, subagents included. Nothing leaves the machine and nothing read is kept.

- **Too early to tell**: a skill installed (or updated, which reinstalls it) during the window is listed apart, not as unused.
- **Usage unknown**: skills not installed for Claude Code. Only its transcripts are read; the other agents' usage is not measured.
- **No transcript in the window** is an error, never "everything is unused".
- `--remove` uninstalls the unused skills listed, from the scopes they are in (`--dry-run` prints the commands); `-g` or `-p` keeps to one scope, `--json` prints `{ since, transcripts_read, transcripts_skipped, unused, used, too_recent, unknown }`, each used skill with its `count` and the day of its `last` use.

It reads only the transcripts changed in the window, and in them only the lines that can hold a call; with several gigabytes of transcripts it still takes tens of seconds. Transcripts above 1 GB are skipped and counted.

### Skills an agent may confuse

An agent chooses a skill from its name and description alone: two installed skills that describe the same job compete for the same requests. `skills-atlas overlaps` compares the descriptions of the installed skills a same agent sees and lists the close pairs, the closest first, with a score from 0 to 1 and the words they share:

```
0.89 review (global) ~ review-bugbot (global)
shared words: bugbot, review, subagent, chang, code · Codex, Cursor, Droid +9
```

Words are weighed by how rare they are among your installed skills, so that “use when the user asks” counts for little. `--threshold <n>` moves the bar (0.5 by default: lower lists more pairs), `-g` or `-p` keeps to one scope, `--json` adds both descriptions. It is a word comparison, in English and French stop words only: it points at pairs worth reading side by side, it does not prove they conflict, and skills of one family (`storage-get`, `storage-delete`) match by design.

### Installation diagnostics

`skills-atlas doctor` checks project and global installations without changing any files. Use `-p` or `-g` for one scope and `--json` for a machine-readable report. It reports broken links, missing or unreadable `SKILL.md` files, invalid lock files and lock entries without a readable installed skill. Missing ordinary agent directories and absent lock files are normal. It exits with 1 when it finds issues and 0 otherwise; it never repairs or removes anything automatically.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "skills-atlas",
"version": "0.30.0",
"version": "0.40.0",
"description": "Browse every public Agent Skill grouped by theme, multi-select and install them through `npx skills add`.",
"type": "module",
"bin": {
Expand Down
74 changes: 73 additions & 1 deletion src/cli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { diagnoseInstalled } from './install/doctor.js';
import { isSuccess, listInstalled, runAll, type CommandOutcome } from './install/run.js';
import { SCOPES, type Catalog, type InstallMethod, type InstallOptions, type Removal, type Scope } from './types.js';
import { readFileSync } from 'node:fs';
import { installedSkills, outdatedSkills, showSkill, skillsByAgent } from './agent-api.js';
import { agentIds, installedSkills, outdatedSkills, showSkill, skillsByAgent } from './agent-api.js';
import { serveMcp } from './mcp.js';
import { complete, completionScript, type CompletionContext } from './completion.js';
import { auditEnabled, auditSkills, auditTarget, failsAudit, withLookalikes } from './audit.js';
Expand All @@ -29,6 +29,8 @@ import { fetchSkillFiles, fetchSkillMarkdown } from './github.js';
import { riskNote, riskWhere } from './risks.js';
import { fileSummary, updateDiff } from './install/diff.js';
import { browseLoop } from './browse.js';
import { describeInstalled, findOverlaps, OVERLAP_THRESHOLD } from './overlaps.js';
import { scanUsage, transcriptsDir, usageReport } from './usage.js';
import { missingSkills, missingSkillsByAgent, parseSelection, parseSelectionFile, toSelection } from './selection.js';

/** The skills a project expects, at its root, versioned with its code. */
Expand Down Expand Up @@ -697,6 +699,76 @@ program
for (const issue of issues) console.log(` ${t().tree.where[issue.scope]} · ${issue.message}: ${issue.path}${issue.name ? ` (${issue.name})` : ''}`);
});

program
.command('unused')
.description(c.unusedCmd)
.option('--days <n>', c.daysOption, '30')
.option('-g, --global', c.global)
.option('-p, --project', c.project)
.option('--remove', c.unusedRemove)
.option('--dry-run', c.dryRun)
.option('--json', c.json)
.action(async (opts: { days: string; global?: boolean; project?: boolean; remove?: boolean; dryRun?: boolean; json?: boolean }) => {
const days = Number(opts.days);
if (!Number.isInteger(days) || days < 1) fail(c.badNumber('--days'));
const scopes: Scope[] = opts.global && !opts.project ? ['global'] : opts.project && !opts.global ? ['project'] : SCOPES;
const since = new Date(Date.now() - days * 86_400_000);
const dir = transcriptsDir();
if (process.stderr.isTTY && !opts.json) console.error(paint('dim', c.usageReading(dir)));
const scan = scanUsage(dir, since);
const report = usageReport((await Promise.all(scopes.map((sc) => listInstalled(sc)))).flat(), scan, since);
const id = (s: { source?: string; name: string }) => `${s.source ?? 'local'}@${s.name}`;
if (opts.json) {
return void process.stdout.write(`${JSON.stringify({
since: since.toISOString().slice(0, 10), transcripts_read: scan.files, transcripts_skipped: scan.skipped,
unused: report.unused.map((s) => ({ id: id(s), name: s.name, scope: s.scope })),
used: report.used.map((s) => ({ id: id(s), name: s.name, scope: s.scope, count: s.count, last: s.last })),
too_recent: report.recent.map((s) => ({ id: id(s), name: s.name, scope: s.scope })),
unknown: report.unknown.map((s) => ({ id: id(s), name: s.name, scope: s.scope })),
}, null, 2)}\n`);
}
// Without a transcript nothing is known: every skill would read as unused.
if (!scan.files) return fail(c.usageNoTranscripts(dir));
console.log(paint('dim', c.usageRead(formatNumber(scan.files), since.toISOString().slice(0, 10))));
if (!report.unused.length) console.log(c.unusedNone);
else {
console.log(paint('bold', c.unusedTitle(report.unused.length, days)));
for (const s of report.unused) console.log(` ${padEnd(s.name, 34)} ${paint('dim', `${padEnd(s.source ?? 'local', 36)} ${t().tree.where[s.scope]}`)}`);
}
if (report.recent.length) console.log(paint('dim', c.unusedRecent(report.recent.map((s) => s.name).join(', '))));
if (report.unknown.length) console.log(paint('dim', c.unusedUnknown(report.unknown.length)));
if (!report.unused.length) return;
if (!opts.remove) return console.log(paint('dim', c.unusedHint));
const removals = new Map<string, Removal>();
for (const s of report.unused) removals.set(s.name, { name: s.name, scopes: [...(removals.get(s.name)?.scopes ?? []), s.scope] });
await apply([], [...removals.values()], { agents: [], scope: 'global', method: 'symlink', skillsVersion: SKILLS_VERSION }, { dryRun: opts.dryRun, mouse: false, skillsVersion: SKILLS_VERSION });
});

program
.command('overlaps')
.description(c.overlapsCmd)
.option('--threshold <n>', c.thresholdOption, String(OVERLAP_THRESHOLD))
.option('-g, --global', c.global)
.option('-p, --project', c.project)
.option('--json', c.json)
.action(async (opts: { threshold: string; global?: boolean; project?: boolean; json?: boolean }) => {
const threshold = Number(opts.threshold);
if (!(threshold > 0 && threshold <= 1)) fail(c.badNumber('--threshold'));
const scopes: Scope[] = opts.global && !opts.project ? ['global'] : opts.project && !opts.global ? ['project'] : SCOPES;
const found = findOverlaps(describeInstalled((await Promise.all(scopes.map((sc) => listInstalled(sc)))).flat()), threshold);
if (opts.json) {
const ref = (s: (typeof found)[number]['a']) => ({ id: `${s.source ?? 'local'}@${s.name}`, name: s.name, scope: s.scope, description: s.description });
return void process.stdout.write(`${JSON.stringify(found.map((o) => ({ score: o.score, a: ref(o.a), b: ref(o.b), shared: o.shared, agents: agentIds(o.agents) })), null, 2)}\n`);
}
if (!found.length) return console.log(paint('dim', c.overlapsNone));
console.log(paint('bold', c.overlapsTitle(found.length)));
for (const o of found) {
console.log(`\n ${paint('yellow', o.score.toFixed(2))} ${o.a.name} ${paint('dim', `(${t().tree.where[o.a.scope]})`)} ~ ${o.b.name} ${paint('dim', `(${t().tree.where[o.b.scope]})`)}`);
console.log(paint('dim', ` ${c.overlapsShared}: ${o.shared.join(', ')} · ${o.agents.slice(0, 3).join(', ')}${o.agents.length > 3 ? ` +${o.agents.length - 3}` : ''}`));
}
console.log(paint('dim', `\n${c.overlapsHint}`));
});

program
.command('installed')
.description(c.installedCmd)
Expand Down
18 changes: 18 additions & 0 deletions src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,24 @@ export const de: Messages = {
showCmd: "gibt die SKILL.md eines Skills aus, gelesen aus seinem GitHub-Repository",
mcpCmd: "stellt Katalog und Installationen als MCP-Tools über stdio bereit, für Claude und andere Agenten",
installedCmd: "listet installierte Skills nach Agent: was jeder Agent sieht",
unusedCmd: "listet installierte Skills, die Claude Code zuletzt nicht benutzt hat, laut seinen Sitzungsprotokollen auf diesem Rechner",
daysOption: "wie viele Tage zurückgeschaut wird (Standard 30)",
unusedRemove: "deinstalliert die aufgelisteten unbenutzten Skills",
overlapsCmd: "listet installierte Skills, deren Beschreibungen so ähnlich sind, dass ein Agent sie verwechseln kann",
thresholdOption: "Ähnlichkeit, ab der ein Paar gelistet wird, 0 bis 1 (Standard 0.5)",
badNumber: (opt) => `${opt}: keine gültige Zahl`,
usageRead: (files, since) => `${files} Claude-Code-Protokolle gelesen, seit ${since}.`,
usageReading: (dir) => `Claude-Code-Protokolle in ${dir} werden gelesen…`,
usageNoTranscripts: (dir) => `Kein Claude-Code-Protokoll aus diesem Zeitraum in ${dir}: Die Nutzung lässt sich nicht messen.`,
unusedNone: "Alle Skills, die Claude Code so lange sieht, wurden benutzt.",
unusedTitle: (n, days) => `Seit ${days} Tagen nicht benutzt: ${n}`,
unusedRecent: (names) => `Seitdem installiert, zu früh für ein Urteil: ${names}`,
unusedUnknown: (n) => `Nicht für Claude Code installiert, Nutzung unbekannt: ${n}`,
unusedHint: "Deinstallieren Sie sie mit `skills-atlas unused --remove` oder wählen Sie sie im Baum ab.",
overlapsNone: "Kein Paar installierter Skills hat ähnliche Beschreibungen.",
overlapsTitle: (n) => `Skill-Paare, die ein Agent verwechseln kann: ${n}`,
overlapsShared: "gemeinsame Wörter",
overlapsHint: "Lesen Sie beide Beschreibungen: Behalten Sie einen Skill, oder lassen Sie jede sagen, wann sie gilt und wann die andere.",
completionCmd: "gibt das Shell-Vervollständigungsskript aus (bash, zsh, fish, powershell)",
packOption: "installiert auch die Skills dieser Pakete (siehe `skills-atlas packs`)",
packsCmd: "listet die Pakete: Skills zum gemeinsamen Installieren",
Expand Down
18 changes: 18 additions & 0 deletions src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,24 @@ export const en = {
showCmd: "print a skill's SKILL.md, read from its GitHub repository",
mcpCmd: "serve the catalog and installs as MCP tools on stdio, for Claude and other agents",
installedCmd: "list installed skills by agent: what each agent sees",
unusedCmd: "list installed skills Claude Code has not used lately, read from its session transcripts on this machine",
daysOption: "how many days back to look (default 30)",
unusedRemove: "uninstall the unused skills listed",
overlapsCmd: "list installed skills whose descriptions are close enough for an agent to confuse them",
thresholdOption: "similarity from which a pair is listed, 0 to 1 (default 0.5)",
badNumber: (opt: string) => `${opt}: not a valid number`,
usageRead: (files: string, since: string) => `${files} Claude Code transcripts read, since ${since}.`,
usageReading: (dir: string) => `Reading the Claude Code transcripts in ${dir}…`,
usageNoTranscripts: (dir: string) => `No Claude Code transcript in that period in ${dir}: usage cannot be measured.`,
unusedNone: "Every skill Claude Code has seen for that long was used.",
unusedTitle: (n: number, days: number) => `Not used in ${days} days: ${n}`,
unusedRecent: (names: string) => `Installed since, too early to tell: ${names}`,
unusedUnknown: (n: number) => `Not installed for Claude Code, usage unknown: ${n}`,
unusedHint: "Uninstall them with `skills-atlas unused --remove`, or untick them in the tree.",
overlapsNone: "No two installed skills have close descriptions.",
overlapsTitle: (n: number) => `Pairs of skills an agent may confuse: ${n}`,
overlapsShared: "shared words",
overlapsHint: "Read both descriptions: keep one skill, or make each say when it applies and when the other does.",
completionCmd: "print the shell completion script (bash, zsh, fish, powershell)",
packOption: "install the skills of these packs too (see `skills-atlas packs`)",
packsCmd: "list the curated packs: skills to install together",
Expand Down
18 changes: 18 additions & 0 deletions src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,24 @@ export const es: Messages = {
showCmd: "muestra el SKILL.md de una skill, leído de su repositorio GitHub",
mcpCmd: "sirve el catálogo y las instalaciones como herramientas MCP por stdio, para Claude y otros agentes",
installedCmd: "lista las skills instaladas por agente: lo que ve cada agente",
unusedCmd: "lista las skills instaladas que Claude Code no ha usado últimamente, según sus transcripciones de sesión en esta máquina",
daysOption: "cuántos días atrás mirar (30 por defecto)",
unusedRemove: "desinstala las skills sin usar listadas",
overlapsCmd: "lista las skills instaladas cuyas descripciones son tan parecidas que un agente puede confundirlas",
thresholdOption: "similitud a partir de la cual se lista un par, de 0 a 1 (0.5 por defecto)",
badNumber: (opt) => `${opt}: número no válido`,
usageRead: (files, since) => `${files} transcripciones de Claude Code leídas, desde el ${since}.`,
usageReading: (dir) => `Leyendo las transcripciones de Claude Code en ${dir}…`,
usageNoTranscripts: (dir) => `Ninguna transcripción de Claude Code en ese periodo en ${dir}: no se puede medir el uso.`,
unusedNone: "Todas las skills que Claude Code ve desde hace ese tiempo se han usado.",
unusedTitle: (n, days) => `Sin usar en ${days} días: ${n}`,
unusedRecent: (names) => `Instaladas desde entonces, demasiado pronto para saberlo: ${names}`,
unusedUnknown: (n) => `No instaladas para Claude Code, uso desconocido: ${n}`,
unusedHint: "Desinstálelas con `skills-atlas unused --remove`, o desmárquelas en el árbol.",
overlapsNone: "Ningún par de skills instaladas tiene descripciones parecidas.",
overlapsTitle: (n) => `Pares de skills que un agente puede confundir: ${n}`,
overlapsShared: "palabras comunes",
overlapsHint: "Lea las dos descripciones: conserve una sola skill, o haga que cada una diga cuándo se aplica y cuándo la otra.",
completionCmd: "muestra el script de autocompletado del shell (bash, zsh, fish, powershell)",
packOption: "instala también las skills de estos packs (ver `skills-atlas packs`)",
packsCmd: "lista los packs: skills para instalar juntas",
Expand Down
Loading
Loading