claude-slim 2.12.3 → 2.13.0

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 CHANGED
@@ -249,13 +249,15 @@ Token counts come from [js-tiktoken](https://github.com/nicolo-ribaudo/js-tiktok
249
249
 
250
250
  ---
251
251
 
252
- ## v2.12.3 — What's new
252
+ ## v2.13.0 — What's new
253
253
 
254
- - **Fixed: the startup total counted a plugin's skills once per cached version.** `claude plugin update` leaves the old version behind as a symlink to the new one (`4.9.1 -> 4.15.4`), and the scan followed both as if they were separate installs inflating the one number this tool exists to report. Both scanners now resolve the version a session actually loads. Measured here: plugin skill entries 148 107, startup total 13,435 → **12,504**.
255
- - **Fixed: every per-plugin cost was doubled when two versions were cached.** The breakdown showed one version's skill count beside two versions' tokens. `oh-my-claudecode` read as ~6,074 tokens against 41 skills; it is ~3,037.
256
- - **Corrected: v2.12.2 reported this issue as "1,944 tokens, 14.5%". It is 931 tokens, 6.9%.** That estimate mistook two distinct plugins shipping identical skill names (`document-skills` and `example-skills` share all 16) for duplicates.
254
+ - **Changed: the startup estimate no longer counts disabled plugins.** Their skills are not in the session catalog, so they are not a startup cost but they were being added to a number labelled "tokens at session start". Verified against a live session rather than assumed: skills from every disabled plugin were absent from the prompt, while every enabled one's were present. Measured here: **12,5049,836**, a 21% correction.
255
+ - **Added `disabledPluginSkillTokens`**, shown under the overhead line and in `--json` what re-enabling everything would cost. A headline number that drops by a fifth with no explanation reads like a bug.
256
+ - **Fixed: plugin skills are attributed to their plugin, not their marketplace.** One marketplace can host several plugins with different enabled states, so the two had to be told apart.
257
257
 
258
- Tests: 445 452 (+7).
258
+ Only plugins *explicitly reported disabled* are excluded. `claude plugin list` has a third state — `✘ failed to load` — and a plugin in it still loads its skills, so anything unrecognised keeps counting.
259
+
260
+ Tests: 452 → 461 (+9).
259
261
 
260
262
  For older release notes, see [CHANGELOG.md](CHANGELOG.md).
261
263
 
package/dist/report.js CHANGED
@@ -251,6 +251,12 @@ export function formatScanSummary(result) {
251
251
  // --- SUMMARY ---
252
252
  lines.push('');
253
253
  lines.push(`\x1b[1m ESTIMATED OVERHEAD\x1b[0m: ~${result.totalTokensBefore.toLocaleString()} tokens at session start`);
254
+ if (result.disabledPluginSkillTokens > 0) {
255
+ // Stated rather than silently omitted: the figure moved out of the total in
256
+ // 2.13.0, and a total that drops with no explanation reads like a bug.
257
+ lines.push(` \x1b[90mexcludes ~${result.disabledPluginSkillTokens.toLocaleString()} tokens of disabled-plugin skills — ` +
258
+ `not loaded, so not a startup cost\x1b[0m`);
259
+ }
254
260
  if (result.issues.length > 0) {
255
261
  // Stated next to the total it is a fraction of, because the numbers on the
256
262
  // issue rows below are body sizes, not startup cost, and adding them up
@@ -90,7 +90,39 @@ export async function scan(opts = {}) {
90
90
  // measured from each file's frontmatter description rather than assumed
91
91
  // (see scanner/skill-listing.ts) — the real spread is 30–500+ tokens apiece.
92
92
  const sumListing = (entries) => entries.reduce((sum, e) => sum + e.listingTokens, 0);
93
- const skillListingTokens = sumListing(localSkills) + sumListing(pluginSkills);
93
+ // A disabled plugin's skills are not in the session catalog, so they cost
94
+ // nothing at startup. Verified against a live session: skills from every
95
+ // disabled plugin here (document-skills, superpowers, telegram, …) were
96
+ // absent from the prompt, while enabled ones were present. Counting them put
97
+ // 3,397 tokens — 27% of the total — into a number labelled "at session start".
98
+ //
99
+ // Only names reported *explicitly disabled* are dropped. `claude plugin list`
100
+ // also emits `failed to load`, which the parser matches as neither enabled nor
101
+ // disabled: railway reports it (a hook clash) and its twelve skills still
102
+ // load. Treating anything unrecognised as disabled would have silently
103
+ // deleted those from the total, so anything not known-disabled still counts.
104
+ // Keyed on `<plugin>@<marketplace>`, not the bare name: the same plugin name
105
+ // can be installed from two marketplaces in different states, and a name-only
106
+ // set would drop the enabled copy along with the disabled one. `pluginName`
107
+ // is the cache directory, which is the marketplace.
108
+ //
109
+ // An identity enabled anywhere is treated as enabled. `claude plugin list`
110
+ // emits one row per scope, so the same identity legitimately appears twice —
111
+ // and counting a live plugin is the safe error to make, not dropping it.
112
+ const pluginIdentity = (plugin, marketplace) => `${plugin}@${marketplace}`;
113
+ const enabledIdentities = new Set(installed.filter((p) => p.enabled).map((p) => pluginIdentity(p.name, p.marketplace)));
114
+ const disabledIdentities = new Set(installed
115
+ .filter((p) => !p.enabled)
116
+ .map((p) => pluginIdentity(p.name, p.marketplace))
117
+ .filter((id) => !enabledIdentities.has(id)));
118
+ const isLoadedAtStartup = (s) => {
119
+ if (s.plugin === undefined || s.pluginName === undefined)
120
+ return true;
121
+ return !disabledIdentities.has(pluginIdentity(s.plugin, s.pluginName));
122
+ };
123
+ const activePluginSkills = pluginSkills.filter(isLoadedAtStartup);
124
+ const disabledPluginSkillTokens = sumListing(pluginSkills.filter((s) => !isLoadedAtStartup(s)));
125
+ const skillListingTokens = sumListing(localSkills) + sumListing(activePluginSkills);
94
126
  const agentListingTokens = sumListing(userSurfaces.agents);
95
127
  const commandListingTokens = sumListing(userSurfaces.commands);
96
128
  // Memory is per-project: a session loads ~/.claude/projects/<slug>/memory/
@@ -138,6 +170,7 @@ export async function scan(opts = {}) {
138
170
  currentProjectMemoryTokens,
139
171
  allProjectsMemoryTokens,
140
172
  recoverableStartupTokens,
173
+ disabledPluginSkillTokens,
141
174
  };
142
175
  }
143
176
  async function pathExists(p) {
@@ -4,17 +4,6 @@ import { getPluginsDir } from '../paths.js';
4
4
  import { safeReadFile, safeReaddir, isDirectory, getDirSize, safeStat } from './fs-walk.js';
5
5
  import { pickActiveVersion } from './plugin-versions.js';
6
6
  import { listingTokensFromContent } from './skill-listing.js';
7
- /**
8
- * The directories under a marketplace whose skills a session would load — one
9
- * per plugin, never two versions of the same one.
10
- *
11
- * The cache is laid out `<marketplace>/<plugin>/<version>/`. A plugin whose
12
- * content sits directly under `<plugin>/` is returned as-is; otherwise its
13
- * children are versions and only the active one counts. That distinction is
14
- * drawn by looking for a `skills` directory rather than by pattern-matching
15
- * version names, so a plugin that ships no skills is simply walked as before
16
- * and contributes nothing either way.
17
- */
18
7
  async function resolveContentRoots(pluginBaseDir) {
19
8
  const entries = await safeReaddir(pluginBaseDir);
20
9
  // Flat layout: the cache entry holds content directly, with no plugin or
@@ -23,7 +12,7 @@ async function resolveContentRoots(pluginBaseDir) {
23
12
  // a candidate version, then picks exactly one — which is not a miscount but a
24
13
  // silent disappearance: the whole entry reported zero skills.
25
14
  if (entries.includes('skills'))
26
- return [pluginBaseDir];
15
+ return [{ dir: pluginBaseDir }];
27
16
  const roots = [];
28
17
  for (const entry of entries) {
29
18
  const pluginDir = join(pluginBaseDir, entry);
@@ -32,7 +21,7 @@ async function resolveContentRoots(pluginBaseDir) {
32
21
  const children = await safeReaddir(pluginDir);
33
22
  if (children.includes('skills')) {
34
23
  // Content root, not a version container.
35
- roots.push(pluginDir);
24
+ roots.push({ dir: pluginDir, plugin: entry });
36
25
  continue;
37
26
  }
38
27
  const versions = [];
@@ -46,11 +35,11 @@ async function resolveContentRoots(pluginBaseDir) {
46
35
  const active = pickActiveVersion(versions);
47
36
  // No subdirectories at all: hand back the plugin dir so the walk behaves
48
37
  // exactly as it did before rather than silently dropping the plugin.
49
- roots.push(active ? active.dir : pluginDir);
38
+ roots.push({ dir: active ? active.dir : pluginDir, plugin: entry });
50
39
  }
51
40
  // A marketplace with no plugin subdirectories still needs walking — some
52
41
  // caches put content directly under the top level.
53
- return roots.length > 0 ? roots : [pluginBaseDir];
42
+ return roots.length > 0 ? roots : [{ dir: pluginBaseDir }];
54
43
  }
55
44
  export async function scanPluginSkills() {
56
45
  const skills = [];
@@ -69,7 +58,7 @@ export async function scanPluginSkills() {
69
58
  return;
70
59
  }
71
60
  const pluginSkillNames = [];
72
- const walkDir = async (dir) => {
61
+ const walkDir = async (dir, plugin) => {
73
62
  // Kept generic: a plugin's content root is normally
74
63
  // `<plugin>/<version>/`, but the walk also has to reach skills nested
75
64
  // deeper. Version selection happens before we get here — see
@@ -97,17 +86,18 @@ export async function scanPluginSkills() {
97
86
  listingTokens: listingTokensFromContent(skillDir, content),
98
87
  source: 'plugin',
99
88
  pluginName,
89
+ plugin,
100
90
  });
101
91
  }
102
92
  }
103
93
  }
104
94
  else {
105
- await walkDir(entryPath);
95
+ await walkDir(entryPath, plugin);
106
96
  }
107
97
  }
108
98
  };
109
99
  for (const root of await resolveContentRoots(pluginDir)) {
110
- await walkDir(root);
100
+ await walkDir(root.dir, root.plugin);
111
101
  }
112
102
  if (pluginSkillNames.length > 0) {
113
103
  plugins.push({
package/dist/types.d.ts CHANGED
@@ -14,7 +14,20 @@ export interface SkillInfo {
14
14
  */
15
15
  listingTokens: number;
16
16
  source: 'local' | 'plugin';
17
+ /**
18
+ * Cache directory this skill was found under — the *marketplace*, not the
19
+ * plugin. Kept as-is because `plugins[]` and the `disabled_plugin` detector
20
+ * match cache directories by this name (see parseDisabledPlugins).
21
+ */
17
22
  pluginName?: string;
23
+ /**
24
+ * The actual plugin, e.g. `oh-my-claudecode` where `pluginName` is `omc`.
25
+ *
26
+ * Needed because enabled/disabled state is reported per plugin, not per
27
+ * marketplace, and one marketplace can host both. Absent when the cache entry
28
+ * has no plugin level to read it from.
29
+ */
30
+ plugin?: string;
18
31
  }
19
32
  export interface BrokenSymlink {
20
33
  name: string;
@@ -109,6 +122,14 @@ export interface ScanResult {
109
122
  * against a 13,434-token startup total. This field is the honest number.
110
123
  */
111
124
  recoverableStartupTokens: number;
125
+ /**
126
+ * Skill-listing tokens belonging to plugins reported as disabled.
127
+ *
128
+ * Excluded from `totalTokensBefore` because a disabled plugin's skills are
129
+ * not in the session catalog. Reported separately so the number does not just
130
+ * vanish: it is what re-enabling everything would cost.
131
+ */
132
+ disabledPluginSkillTokens: number;
112
133
  }
113
134
  export interface ManifestEntry {
114
135
  date: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-slim",
3
- "version": "2.12.3",
3
+ "version": "2.13.0",
4
4
  "description": "Audit and shrink your Claude Code startup context. Measures what every skill, plugin, agent, command, and memory file costs in the system prompt, then reversibly disables the dead weight. Non-destructive scan, tiered proposals, one-command restore — no proxy, no compression.",
5
5
  "type": "module",
6
6
  "bin": {