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 +7 -5
- package/dist/report.js +6 -0
- package/dist/scanner/index.js +34 -1
- package/dist/scanner/plugin-skills.js +8 -18
- package/dist/types.d.ts +21 -0
- package/package.json +1 -1
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.
|
|
252
|
+
## v2.13.0 — What's new
|
|
253
253
|
|
|
254
|
-
- **
|
|
255
|
-
- **
|
|
256
|
-
- **
|
|
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,504 → 9,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
|
-
|
|
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
|
package/dist/scanner/index.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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": {
|