stack-cleaner 1.1.5 → 1.2.1
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/README.md +10 -1
- package/package.json +1 -1
- package/public/scan.mjs +33 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Free · open source · runs locally · sends nothing.
|
|
|
14
14
|
|
|
15
15
|
Over time, Claude Code accumulates a lot: user-level skills in `~/.claude`, project skills in every repo's `.claude`, plugins from a handful of marketplaces, MCP servers wired into different projects, and a pile of sub-agents. It gets hard to answer simple questions: *what do I actually have? what do I never use? what's duplicated? what can I safely remove?*
|
|
16
16
|
|
|
17
|
-
This tool answers them. You run one command, it reads your local Claude Code install, and the web app shows everything **grouped by where it lives** (global vs. each project), with **real usage counts**, so you can spot the dead weight and build a one-click cleanup plan.
|
|
17
|
+
This tool answers them. You run one command, it reads your local Claude Code install, and the web app shows everything **grouped by where it lives** (global vs. each project), with **real usage counts** and **automatic duplicate detection**, so you can spot the dead weight, see what's redundant, and build a one-click cleanup plan.
|
|
18
18
|
|
|
19
19
|
## How it works
|
|
20
20
|
|
|
@@ -83,6 +83,15 @@ It enumerates **every project** Claude Code knows about (from `~/.claude.json`),
|
|
|
83
83
|
|
|
84
84
|
The scan reads your local Claude Code **transcripts** (`~/.claude/projects/*.jsonl`) to count real invocations for **skills, agents, and MCP servers**, so you can finally see what's *installed but never used*, even for the types that carry no usage count in plain config. It extracts **only the tool / skill / agent / MCP-server names, the counts, and the timestamps**, never your prompts, message text, tool arguments, file paths, or command contents. It all stays local until you choose to upload the file. Pass `--no-transcripts` to skip the transcript read entirely. (Skills and plugins also keep their counts from Claude Code's own `skillUsage` / `pluginUsage` tables.)
|
|
85
85
|
|
|
86
|
+
## Finding duplicates
|
|
87
|
+
|
|
88
|
+
Claude Code makes it easy to end up with the same thing twice — a standalone skill you installed by hand that a plugin now bundles, the same MCP server wired up from two sources, or same-named copies across global and a project. Stack Cleaner flags these **automatically**:
|
|
89
|
+
|
|
90
|
+
- **Superseded by a plugin** — a standalone skill, agent, or MCP server that an installed plugin already provides. The plugin's copy stays updated; the loose one won't, so it's the safe one to remove.
|
|
91
|
+
- **Same-name duplicates** and **duplicate MCP servers** — the same item installed two different ways.
|
|
92
|
+
|
|
93
|
+
Every flagged item explains, in plain language, *what* it duplicates, *where* your copy lives, and *which* one to keep. A **Duplicates** filter and a **"Select N redundant copies"** button turn the whole pile into a one-click cleanup.
|
|
94
|
+
|
|
86
95
|
## Privacy & safety
|
|
87
96
|
|
|
88
97
|
This is the part that matters, because the output describes your tooling.
|
package/package.json
CHANGED
package/public/scan.mjs
CHANGED
|
@@ -35,8 +35,8 @@ import os from "node:os";
|
|
|
35
35
|
import readline from "node:readline";
|
|
36
36
|
import { pathToFileURL } from "node:url";
|
|
37
37
|
|
|
38
|
-
const SCHEMA_VERSION =
|
|
39
|
-
const GENERATOR = "scan.mjs@1.1
|
|
38
|
+
const SCHEMA_VERSION = 2;
|
|
39
|
+
const GENERATOR = "scan.mjs@1.2.1";
|
|
40
40
|
const HOME = os.homedir();
|
|
41
41
|
const CLAUDE = path.join(HOME, ".claude");
|
|
42
42
|
|
|
@@ -397,6 +397,35 @@ function buildProjectLabels(paths) {
|
|
|
397
397
|
return labels;
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
// Enumerate what an installed plugin bundles, by reading its on-disk dir:
|
|
401
|
+
// <installDir>/skills/<name>/SKILL.md → skill names
|
|
402
|
+
// <installDir>/agents/<name>.md → agent names
|
|
403
|
+
// <installDir>/.mcp.json mcpServers → mcp server names
|
|
404
|
+
// Names only — no contents. Returns undefined when nothing is found.
|
|
405
|
+
export function collectPluginBundles(installDir) {
|
|
406
|
+
if (!installDir || !exists(installDir)) return undefined;
|
|
407
|
+
const skills = [];
|
|
408
|
+
const skillsDir = path.join(installDir, "skills");
|
|
409
|
+
for (const d of listDir(skillsDir, "dir")) {
|
|
410
|
+
if (exists(path.join(skillsDir, d, "SKILL.md"))) skills.push(d);
|
|
411
|
+
}
|
|
412
|
+
const agents = [];
|
|
413
|
+
const agentsDir = path.join(installDir, "agents");
|
|
414
|
+
for (const f of listDir(agentsDir, "file")) {
|
|
415
|
+
if (f.endsWith(".md")) agents.push(f.replace(/\.md$/, ""));
|
|
416
|
+
}
|
|
417
|
+
const mcps = [];
|
|
418
|
+
const mcpCfg = readJSON(path.join(installDir, ".mcp.json"));
|
|
419
|
+
if (mcpCfg && mcpCfg.mcpServers && typeof mcpCfg.mcpServers === "object") {
|
|
420
|
+
for (const name of Object.keys(mcpCfg.mcpServers)) mcps.push(name);
|
|
421
|
+
}
|
|
422
|
+
const out = {};
|
|
423
|
+
if (skills.length) out.skills = skills;
|
|
424
|
+
if (agents.length) out.agents = agents;
|
|
425
|
+
if (mcps.length) out.mcps = mcps;
|
|
426
|
+
return Object.keys(out).length ? out : undefined;
|
|
427
|
+
}
|
|
428
|
+
|
|
400
429
|
// ---------- inventory build ----------
|
|
401
430
|
// Collects every skill / agent / plugin / MCP item from the local install,
|
|
402
431
|
// optionally overlays transcript usage, and returns the inventory OBJECT.
|
|
@@ -504,6 +533,7 @@ export async function buildInventory(opts = {}) {
|
|
|
504
533
|
const installPath = entry && entry.installPath
|
|
505
534
|
? (entry.installPath.startsWith(HOME) ? tilde(entry.installPath) : "<external>")
|
|
506
535
|
: undefined;
|
|
536
|
+
const bundles = entry && entry.installPath ? collectPluginBundles(entry.installPath) : undefined;
|
|
507
537
|
items.push({
|
|
508
538
|
// Bare name; the marketplace lives in `source` (matches the demo shape).
|
|
509
539
|
id: `plugin:global:${pluginName}`,
|
|
@@ -513,6 +543,7 @@ export async function buildInventory(opts = {}) {
|
|
|
513
543
|
source: marketplace || "",
|
|
514
544
|
version: entry ? entry.version : undefined,
|
|
515
545
|
path: installPath,
|
|
546
|
+
bundles,
|
|
516
547
|
usageCount, lastUsedAt, usageClass,
|
|
517
548
|
usageLabel: usageLabel(usageCount, lastUsedAt, usageClass),
|
|
518
549
|
// The CLI wants the full name@marketplace form to uninstall.
|