claude-slim 2.9.0 → 2.9.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
@@ -245,17 +245,14 @@ Token counts come from [js-tiktoken](https://github.com/nicolo-ribaudo/js-tiktok
245
245
 
246
246
  ---
247
247
 
248
- ## v2.8.0 — What's new
248
+ ## v2.9.1 — What's new
249
249
 
250
- Accuracy release. Three reported numbers were wrong; the largest was wrong by an order of magnitude. **If your startup estimate drops sharply after upgrading, the old number was the inaccurate one.**
250
+ Found while stress-testing the scanner against deliberately hostile `~/.claude` fixtures.
251
251
 
252
- - **Startup estimate no longer sums memory across every project on disk.** Claude Code loads `~/.claude/projects/<slug>/memory/` for the project you're in not the other 40 project directories in your `~/.claude`. The old total scaled with how many projects you'd ever opened: on the dev machine it reported **116,259 tokens where the real per-session cost was 14,399**. Now scoped to the current project, with the cross-project total still shown and labelled as not a per-session cost.
253
- - **Skill listing cost is measured, not assumed.** Each skill adds a `- <name>: <description>` line to the system prompt. The flat 30-tokens-per-skill estimate stood in for all of them; measured across 68 installed skills the real spread is **30 → 509 tokens (mean 51)**. The per-plugin cost gradient can now tell five terse skills apart from five verbose ones.
254
- - **`~/.claude/agents/` and `~/.claude/commands/` are now scanned.** Previously invisible despite loading into every session — 12 agents worth ~2,254 tokens on the dev machine. Reported only; never moved or deleted, because there's no restore path for them yet.
255
- - **Fixed: plugin manifests were stuck at 2.7.0 for three releases**, so `claude plugin install` advertised a stale version. CI now fails on version drift.
256
- - **Fixed: the token cache grew without bound** — 355 of 776 entries (46%) pointed at deleted files. `flushCache()` now prunes them.
252
+ - **Fixed: `scan` could hang forever on a single long line.** js-tiktoken's BPE is quadratic in the length of one whitespace-free run. Normal prose is fine (8,000 characters encodes in ~1ms), but 800 characters of Hangul cost ~450ms, 3,200 cost ~6.8s, and a 60,000-character run wedged `scan` past 20 seconds with **no output at all** indistinguishable from a freeze. `SKILL.md` files hit this via base64 blobs, minified snippets, embedded JSON schemas, and CJK text. Long runs are now estimated by encoding a bounded prefix and scaling; **files without one produce byte-identical counts** (verified across 71 installed skills: 70 exact, one moved 0.01%). Hostile fixture: 20s+ → 1.78s, with no measurable cost on ordinary input.
253
+ - **Fixed: a mistyped subcommand blamed the wrong thing.** `claude-slim scam` printed `error: too many arguments. Expected 0 arguments but got 1`. It now names the unknown command and lists the real ones.
257
254
 
258
- Tests: 206241 (+35).
255
+ Tests: 266279 (+13).
259
256
 
260
257
  For older release notes, see [CHANGELOG.md](CHANGELOG.md).
261
258
 
package/dist/cli.js CHANGED
@@ -244,7 +244,21 @@ program
244
244
  await flushCache();
245
245
  });
246
246
  // --- default (no subcommand) → run clean ---
247
+ // Because the program itself carries an action, commander routes a mistyped
248
+ // subcommand here and reports its own "too many arguments. Expected 0 arguments
249
+ // but got 1" — which says nothing about what the user actually got wrong.
250
+ // Accept the excess argument so we can name it instead.
251
+ program.allowExcessArguments(true);
247
252
  program.action(async () => {
253
+ const stray = program.args;
254
+ if (stray.length > 0) {
255
+ const names = program.commands.map((c) => c.name()).join(', ');
256
+ console.error(`error: unknown command '${stray[0]}'`);
257
+ console.error(`available commands: ${names}`);
258
+ console.error(`run 'claude-slim --help' for usage`);
259
+ process.exitCode = 1;
260
+ return;
261
+ }
248
262
  await runCleanPipeline({ dryRun: false, auto: false, sessionsPerDay: 2, lookbackDays: 60 });
249
263
  });
250
264
  // --- shared clean pipeline ---
package/dist/tokenizer.js CHANGED
@@ -39,11 +39,72 @@ export async function initTokenizer() {
39
39
  function hashContent(content) {
40
40
  return createHash('md5').update(content).digest('hex');
41
41
  }
42
+ // js-tiktoken's BPE is quadratic in the length of a single whitespace-free run.
43
+ // Ordinary prose of any length is fine — the pre-tokenizer splits on whitespace,
44
+ // so 8,000 characters of normal text encodes in ~1ms. One long unbroken run does
45
+ // not: measured on cl100k_base, 800 characters of Hangul costs ~450ms, 3,200
46
+ // costs ~6.8s, and a 60,000-character run wedges the scan for minutes with no
47
+ // output at all. Real SKILL.md files reach this with base64 blobs, minified
48
+ // snippets, rule separators, and CJK text.
49
+ //
50
+ // 512 sits above anything a normal word, path, or URL produces and well below
51
+ // where the curve turns painful.
52
+ const MAX_ENCODE_RUN = 512;
53
+ const LONG_RUN_PATTERN = /\S{513,}/;
54
+ const FALLBACK_CHARS_PER_TOKEN = 4;
55
+ /**
56
+ * Estimate an over-long run by encoding a bounded prefix and scaling.
57
+ *
58
+ * A fixed characters-per-token divisor cannot work here: measured over 1,000-char
59
+ * runs, cl100k_base yields 0.8 chars/token for Hangul but 8.0 for a repeated
60
+ * ASCII character — a 10× spread. Sampling the run's own prefix adapts to
61
+ * whatever it actually contains (base64, hex, minified JSON, CJK) at the cost of
62
+ * exactly one bounded encode.
63
+ */
64
+ function estimateRun(segment, encode) {
65
+ const sample = segment.slice(0, MAX_ENCODE_RUN);
66
+ const sampleTokens = encode(sample).length;
67
+ if (sampleTokens === 0)
68
+ return 0;
69
+ return Math.ceil((sampleTokens * segment.length) / sample.length);
70
+ }
71
+ /**
72
+ * Encode `text`, estimating any whitespace-free run longer than
73
+ * {@link MAX_ENCODE_RUN} instead of feeding the whole run to the BPE.
74
+ *
75
+ * Text without such a run takes the fast path and is encoded whole, so counts
76
+ * for well-formed files are identical to encoding directly.
77
+ */
78
+ function encodeBounded(text, encode) {
79
+ if (!LONG_RUN_PATTERN.test(text)) {
80
+ return encode(text).length;
81
+ }
82
+ let total = 0;
83
+ let buffered = '';
84
+ // Splitting on a captured group keeps the whitespace in the stream, so the
85
+ // buffered pieces still look to the encoder like the original text.
86
+ for (const segment of text.split(/(\s+)/)) {
87
+ if (segment.length > MAX_ENCODE_RUN) {
88
+ if (buffered) {
89
+ total += encode(buffered).length;
90
+ buffered = '';
91
+ }
92
+ total += estimateRun(segment, encode);
93
+ continue;
94
+ }
95
+ buffered += segment;
96
+ }
97
+ if (buffered) {
98
+ total += encode(buffered).length;
99
+ }
100
+ return total;
101
+ }
42
102
  export function countTokens(text) {
43
103
  if (useFallback || !encoder) {
44
- return Math.ceil(text.length / 4);
104
+ return Math.ceil(text.length / FALLBACK_CHARS_PER_TOKEN);
45
105
  }
46
- return encoder.encode(text).length;
106
+ const enc = encoder;
107
+ return encodeBounded(text, (s) => enc.encode(s));
47
108
  }
48
109
  export function countTokensCached(text, filePath) {
49
110
  const hash = hashContent(text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-slim",
3
- "version": "2.9.0",
3
+ "version": "2.9.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": {