claude-slim 2.12.3 → 2.13.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 CHANGED
@@ -94,21 +94,21 @@ That's slower responses. Hitting your usage cap faster. Paying for context you'r
94
94
  **Report** — Shows exactly what changed:
95
95
 
96
96
  ```
97
- ╭──────────────────────────────────────────╮
98
- │ claude-slim report
99
-
100
- │ Before: 14,510 tokens at startup
101
- │ After: 5,181 tokens at startup
102
- │ Saved: 9,329 tokens (64.3%)
103
-
104
- │ Top offenders removed:
105
- │ • office-hours 23,008 tok
106
- │ • harness 7,902 tok
107
- │ • manpower 4,764 tok
108
-
109
- │ Est. monthly savings: ~$1.68
110
- │ (2 sessions/day × $0.003/1K tok)
111
- ╰──────────────────────────────────────────╯
97
+ ╭────────────────────────────────────────╮
98
+ │ claude-slim report
99
+
100
+ │ Before: 14,510 tokens at startup
101
+ │ After: 5,181 tokens at startup
102
+ │ Saved: 9,329 tokens (64.3%)
103
+
104
+ │ Top offenders removed:
105
+ │ • office-hours 23,008 tok│
106
+ │ • harness 7,902 tok│
107
+ │ • manpower 4,764 tok│
108
+
109
+ │ Est. monthly savings: ~$1.68
110
+ │ (2 sessions/day × $0.003/1K tok)
111
+ ╰────────────────────────────────────────╯
112
112
 
113
113
  ┌──────────────────┬──────────┬──────────┬────────────┐
114
114
  │ │ Before │ After │ Saved │
@@ -249,13 +249,14 @@ 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.1 — 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
+ - **Fixed: `scan` promised the tool never touches `~/.codex/`, while `clean --auto` deletes from it permanently.** The Codex summary closed with `Reported only claude-slim never modifies ~/.codex/.` That was true in v2.10 and false from **v2.11** on, when `clean` gained the Codex tiers. Measured here: `clean --dry-run` selects `[Auto] [codex] temp_cache: .tmp (146MB of install leftovers) (permanent)` Tier 1, which `--auto` applies without prompting and no `restore` undoes.
255
+ - The summary now says what actually happens: `scan` only reads, `clean` acts under the same tiers as `~/.claude/`, moves land in `~/.codex/skills.disabled/` and reverse with `restore`, and Tier 1 install leftovers are deleted permanently.
257
256
 
258
- Tests: 445 452 (+7).
257
+ A test was holding the false claim in place. It asserted the summary *must* say "never modifies", so the line could not be corrected without a test failing — and all 461 tests stayed green across four releases that contradicted it. The assertion is now inverted: the summary must not promise `~/.codex/` is left alone, and must name the permanent deletion and `--auto`.
258
+
259
+ Tests: 461 → 462.
259
260
 
260
261
  For older release notes, see [CHANGELOG.md](CHANGELOG.md).
261
262
 
package/dist/cli.js CHANGED
@@ -50,8 +50,9 @@ program
50
50
  lookbackDays: parseNonNegativeInt(opts.lookbackDays, 60),
51
51
  projectDir,
52
52
  });
53
- // Codex is scanned when present. Reported only it is never modified, and
54
- // unused-skill detection is suppressed there for lack of a usage signal.
53
+ // Codex is scanned when present. `scan` only reads; `clean` has acted here
54
+ // since v2.11 under the same tiers. Unused-skill detection stays suppressed
55
+ // for lack of a usage signal.
55
56
  // commander maps `--no-codex` to `opts.codex === false`, not `opts.noCodex`.
56
57
  const codex = opts.codex === false ? null : await scanCodex();
57
58
  await flushCache();
@@ -44,13 +44,15 @@ export function formatCodexSummary(result) {
44
44
  lines.push(` ${b.name.length > 30 ? `${b.name.slice(0, 29)}…` : b.name.padEnd(30)} ` +
45
45
  `${String(b.listingTokens).padStart(5)} tok \x1b[90m${b.backupArtifact}\x1b[0m`);
46
46
  }
47
- lines.push(` \x1b[90mNot removed — ~/.codex/ is read-only here. Delete manually if stale.\x1b[0m`);
47
+ lines.push(` \x1b[90mOffered by clean as Tier 2 moved to ~/.codex/skills.disabled/, reversible with restore.\x1b[0m`);
48
48
  }
49
49
  lines.push('');
50
50
  lines.push(` \x1b[33m!\x1b[0m Unused-skill detection unavailable for Codex.`);
51
51
  lines.push(` \x1b[90m${result.unusedDetectionReason}\x1b[0m`);
52
52
  lines.push('');
53
- lines.push(` \x1b[90mReported only claude-slim never modifies ~/.codex/.\x1b[0m`);
53
+ lines.push(` \x1b[90mscan only reads. Since v2.11 clean also acts here, under the same tiers as ~/.claude/:\x1b[0m`);
54
+ lines.push(` \x1b[90mmoves go to ~/.codex/skills.disabled/ and reverse with restore, but Tier 1 install\x1b[0m`);
55
+ lines.push(` \x1b[90mleftovers (~/.codex/.tmp) are deleted permanently, and --auto applies Tier 1 unprompted.\x1b[0m`);
54
56
  lines.push('');
55
57
  return lines.join('\n');
56
58
  }
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.1",
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": {
@@ -65,6 +65,6 @@
65
65
  "@types/node": "^22.20.1",
66
66
  "tmp-promise": "^3.0.3",
67
67
  "typescript": "^5.9.3",
68
- "vitest": "^4.1.10"
68
+ "vitest": "^4.1.11"
69
69
  }
70
70
  }