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 +5 -8
- package/dist/cli.js +14 -0
- package/dist/tokenizer.js +63 -2
- package/package.json +1 -1
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.
|
|
248
|
+
## v2.9.1 — What's new
|
|
249
249
|
|
|
250
|
-
|
|
250
|
+
Found while stress-testing the scanner against deliberately hostile `~/.claude` fixtures.
|
|
251
251
|
|
|
252
|
-
- **
|
|
253
|
-
- **
|
|
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:
|
|
255
|
+
Tests: 266 → 279 (+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 /
|
|
104
|
+
return Math.ceil(text.length / FALLBACK_CHARS_PER_TOKEN);
|
|
45
105
|
}
|
|
46
|
-
|
|
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.
|
|
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": {
|