claude-slim 2.8.1 → 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 +22 -8
- package/dist/cli.js +42 -0
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +32 -0
- package/dist/tokenizer.js +63 -2
- package/dist/update-check.d.ts +38 -0
- package/dist/update-check.js +177 -0
- package/package.json +1 -1
- package/skills/claude-slim/SKILL.md +28 -0
package/README.md
CHANGED
|
@@ -149,6 +149,7 @@ Then just type `/claude-slim` in any session.
|
|
|
149
149
|
/claude-slim scan --json # Machine-readable JSON output
|
|
150
150
|
/claude-slim scan --lookback-days 30 # Treat skills idle for 30+ days as unused
|
|
151
151
|
/claude-slim doctor # Check scanner prerequisites and data fidelity
|
|
152
|
+
/claude-slim check-update # Is a newer version published?
|
|
152
153
|
/claude-slim restore # Bring back anything you disabled
|
|
153
154
|
```
|
|
154
155
|
|
|
@@ -161,10 +162,26 @@ npx claude-slim clean --auto # Non-interactive, Tier 1 only (CI/scri
|
|
|
161
162
|
npx claude-slim clean --lookback-days N # Tune the unused-skill detection window
|
|
162
163
|
npx claude-slim scan # Report only
|
|
163
164
|
npx claude-slim doctor # Diagnose Node/Claude/session-log readiness
|
|
165
|
+
npx claude-slim doctor --offline # Same, without the version check (no network)
|
|
166
|
+
npx claude-slim check-update # Report-only version check
|
|
164
167
|
npx claude-slim restore # Undo
|
|
165
168
|
npx claude-slim report # Show savings from last clean
|
|
166
169
|
```
|
|
167
170
|
|
|
171
|
+
### Staying current
|
|
172
|
+
|
|
173
|
+
An outdated claude-slim doesn't just miss features — it reports **wrong numbers**. Versions before 2.8.0 inflated the startup estimate roughly 8×, and nothing told you that you were behind.
|
|
174
|
+
|
|
175
|
+
`doctor` now compares your installed version against npm and prints the upgrade command for how you actually installed it:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
! Version: 2.0.0 installed, 2.8.1 available
|
|
179
|
+
Outdated versions report wrong token totals. Update with:
|
|
180
|
+
claude plugin marketplace update claude-slim && claude plugin update claude-slim@claude-slim
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
claude-slim never updates itself — that's your package manager's job, and writing into a directory `claude plugin` owns is how installs get corrupted. It only tells you. The check is the tool's only outbound request, is skipped with `--offline`, caches for 24h, and fails open when you're offline.
|
|
184
|
+
|
|
168
185
|
---
|
|
169
186
|
|
|
170
187
|
## Safety first
|
|
@@ -228,17 +245,14 @@ Token counts come from [js-tiktoken](https://github.com/nicolo-ribaudo/js-tiktok
|
|
|
228
245
|
|
|
229
246
|
---
|
|
230
247
|
|
|
231
|
-
## v2.
|
|
248
|
+
## v2.9.1 — What's new
|
|
232
249
|
|
|
233
|
-
|
|
250
|
+
Found while stress-testing the scanner against deliberately hostile `~/.claude` fixtures.
|
|
234
251
|
|
|
235
|
-
- **
|
|
236
|
-
- **
|
|
237
|
-
- **`~/.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.
|
|
238
|
-
- **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.
|
|
239
|
-
- **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.
|
|
240
254
|
|
|
241
|
-
Tests:
|
|
255
|
+
Tests: 266 → 279 (+13).
|
|
242
256
|
|
|
243
257
|
For older release notes, see [CHANGELOG.md](CHANGELOG.md).
|
|
244
258
|
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { cleanIssues, restoreItem } from './cleaner.js';
|
|
|
10
10
|
import { readManifest } from './manifest.js';
|
|
11
11
|
import { formatScanSummary, formatReportBox, calculateReport, } from './report.js';
|
|
12
12
|
import { collectDoctorReport, formatDoctorReport } from './doctor.js';
|
|
13
|
+
import { checkForUpdate, formatUpdateNotice } from './update-check.js';
|
|
13
14
|
import { resolveSelection, resolveRestoreSelection } from './selection.js';
|
|
14
15
|
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
15
16
|
const { version: PKG_VERSION } = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
@@ -52,9 +53,11 @@ program
|
|
|
52
53
|
.description('Check local Claude Code environment and scanner fidelity')
|
|
53
54
|
.option('--json', 'Output raw JSON')
|
|
54
55
|
.option('--lookback-days <n>', 'Days of session history for skill-usage analysis', '60')
|
|
56
|
+
.option('--offline', 'Skip the npm version check (no outbound request)')
|
|
55
57
|
.action(async (opts) => {
|
|
56
58
|
const report = await collectDoctorReport({
|
|
57
59
|
lookbackDays: parseNonNegativeInt(opts.lookbackDays, 60),
|
|
60
|
+
checkUpdate: !opts.offline,
|
|
58
61
|
});
|
|
59
62
|
if (opts.json) {
|
|
60
63
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -63,6 +66,31 @@ program
|
|
|
63
66
|
console.log(formatDoctorReport(report));
|
|
64
67
|
}
|
|
65
68
|
});
|
|
69
|
+
// --- check-update ---
|
|
70
|
+
// Detection only. Updating is the package manager's job — claude-slim writing
|
|
71
|
+
// into a directory `claude plugin` owns is how installs get corrupted.
|
|
72
|
+
program
|
|
73
|
+
.command('check-update')
|
|
74
|
+
.description('Check whether a newer claude-slim is published (no changes made)')
|
|
75
|
+
.option('--json', 'Output raw JSON')
|
|
76
|
+
.option('--force', 'Ignore the 24h cache and re-query the registry')
|
|
77
|
+
.action(async (opts) => {
|
|
78
|
+
const result = await checkForUpdate({ force: Boolean(opts.force) });
|
|
79
|
+
if (opts.json) {
|
|
80
|
+
console.log(JSON.stringify(result, null, 2));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const notice = formatUpdateNotice(result);
|
|
84
|
+
if (notice) {
|
|
85
|
+
console.log(`\n \x1b[33m${notice}\x1b[0m\n`);
|
|
86
|
+
}
|
|
87
|
+
else if (result.latest === null) {
|
|
88
|
+
console.log(`\n Could not reach the npm registry. Installed: ${result.installed}\n`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.log(`\n \x1b[32m✓\x1b[0m claude-slim ${result.installed} is up to date.\n`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
66
94
|
// --- clean ---
|
|
67
95
|
program
|
|
68
96
|
.command('clean')
|
|
@@ -216,7 +244,21 @@ program
|
|
|
216
244
|
await flushCache();
|
|
217
245
|
});
|
|
218
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);
|
|
219
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
|
+
}
|
|
220
262
|
await runCleanPipeline({ dryRun: false, auto: false, sessionsPerDay: 2, lookbackDays: 60 });
|
|
221
263
|
});
|
|
222
264
|
// --- shared clean pipeline ---
|
package/dist/doctor.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type UpdateCheckResult } from './update-check.js';
|
|
1
2
|
export type DoctorStatus = 'ok' | 'warn' | 'fail';
|
|
2
3
|
export interface DoctorCheck {
|
|
3
4
|
label: string;
|
|
@@ -11,5 +12,6 @@ export interface DoctorReport {
|
|
|
11
12
|
export declare function isSupportedRuntimeNode(version: string): boolean;
|
|
12
13
|
export declare function collectDoctorReport(opts?: {
|
|
13
14
|
lookbackDays?: number;
|
|
15
|
+
checkUpdate?: boolean | (() => Promise<UpdateCheckResult>);
|
|
14
16
|
}): Promise<DoctorReport>;
|
|
15
17
|
export declare function formatDoctorReport(report: DoctorReport): string;
|
package/dist/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@ import { access, readdir } from 'node:fs/promises';
|
|
|
2
2
|
import { getClaudeDir, getPluginsDir, getProjectsDir, getSkillsDir } from './paths.js';
|
|
3
3
|
import { runCommand } from './scanner/fs-walk.js';
|
|
4
4
|
import { scanSessionUsage } from './scanner/sessions.js';
|
|
5
|
+
import { checkForUpdate } from './update-check.js';
|
|
5
6
|
const MIN_RUNTIME_NODE_MAJOR = 20;
|
|
6
7
|
export function isSupportedRuntimeNode(version) {
|
|
7
8
|
const normalized = version.trim().replace(/^v/, '');
|
|
@@ -87,6 +88,37 @@ export async function collectDoctorReport(opts = {}) {
|
|
|
87
88
|
: 'Unused-skill detection will be suppressed until enough reliable session data exists.',
|
|
88
89
|
});
|
|
89
90
|
}
|
|
91
|
+
// Version drift, last: it is the only check that touches the network, and it
|
|
92
|
+
// is opt-in so `doctor` stays offline-safe when the caller wants that.
|
|
93
|
+
if (opts.checkUpdate) {
|
|
94
|
+
const run = typeof opts.checkUpdate === 'function' ? opts.checkUpdate : checkForUpdate;
|
|
95
|
+
const update = await run();
|
|
96
|
+
if (update.latest === null) {
|
|
97
|
+
checks.push({
|
|
98
|
+
label: 'Version',
|
|
99
|
+
status: 'warn',
|
|
100
|
+
detail: `${update.installed} installed, latest unknown`,
|
|
101
|
+
hint: 'Could not reach the npm registry. This does not affect scanning.',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
else if (update.outdated) {
|
|
105
|
+
checks.push({
|
|
106
|
+
label: 'Version',
|
|
107
|
+
status: 'warn',
|
|
108
|
+
detail: `${update.installed} installed, ${update.latest} available`,
|
|
109
|
+
hint: update.upgradeCommand
|
|
110
|
+
? `Outdated versions report wrong token totals. Update with: ${update.upgradeCommand}`
|
|
111
|
+
: 'Outdated versions report wrong token totals — update via your package manager.',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
checks.push({
|
|
116
|
+
label: 'Version',
|
|
117
|
+
status: 'ok',
|
|
118
|
+
detail: `${update.installed} (latest)`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
90
122
|
return { checks };
|
|
91
123
|
}
|
|
92
124
|
export function formatDoctorReport(report) {
|
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);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type InstallMethod = 'plugin' | 'global' | 'npx' | 'source' | 'unknown';
|
|
2
|
+
export interface UpdateCheckResult {
|
|
3
|
+
installed: string;
|
|
4
|
+
/** null when the lookup failed or was skipped — never treat as "up to date". */
|
|
5
|
+
latest: string | null;
|
|
6
|
+
outdated: boolean;
|
|
7
|
+
installMethod: InstallMethod;
|
|
8
|
+
/** The command that actually upgrades this install, or null if unknown. */
|
|
9
|
+
upgradeCommand: string | null;
|
|
10
|
+
fromCache: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Compare dotted numeric versions. Returns >0 if a is newer, <0 if b is newer.
|
|
14
|
+
* Pre-release suffixes (`-beta.1`) sort below the same release, matching semver
|
|
15
|
+
* closely enough for "is there something newer" without pulling in a dep.
|
|
16
|
+
*/
|
|
17
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
18
|
+
/**
|
|
19
|
+
* Infer how this copy was installed from where it sits on disk, so the hint we
|
|
20
|
+
* print is the command that will actually work for this user.
|
|
21
|
+
*/
|
|
22
|
+
export declare function detectInstallMethod(modulePath: string): InstallMethod;
|
|
23
|
+
export declare function upgradeCommandFor(method: InstallMethod): string | null;
|
|
24
|
+
export declare function getInstalledVersion(): string;
|
|
25
|
+
export interface CheckOptions {
|
|
26
|
+
installed?: string;
|
|
27
|
+
modulePath?: string;
|
|
28
|
+
cachePath?: string;
|
|
29
|
+
now?: number;
|
|
30
|
+
ttlMs?: number;
|
|
31
|
+
/** Ignore a fresh cache entry and re-query. */
|
|
32
|
+
force?: boolean;
|
|
33
|
+
/** Injected for tests; defaults to the real registry lookup. */
|
|
34
|
+
fetchLatest?: () => Promise<string | null>;
|
|
35
|
+
}
|
|
36
|
+
export declare function checkForUpdate(opts?: CheckOptions): Promise<UpdateCheckResult>;
|
|
37
|
+
/** One-line human summary; null when there is nothing worth saying. */
|
|
38
|
+
export declare function formatUpdateNotice(result: UpdateCheckResult): string | null;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFile, writeFile, mkdir, rename } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { getClaudeDir } from './paths.js';
|
|
6
|
+
// Version-drift detection.
|
|
7
|
+
//
|
|
8
|
+
// Running an outdated claude-slim is not a cosmetic problem: v2.8.0 corrected
|
|
9
|
+
// a startup estimate that earlier versions inflated ~8x, so a stale install
|
|
10
|
+
// reports numbers that are simply wrong. Nothing told users they were behind —
|
|
11
|
+
// this module closes that gap.
|
|
12
|
+
//
|
|
13
|
+
// It only *detects*. Updating is the package manager's job (`claude plugin
|
|
14
|
+
// update`, `npm update -g`), and claude-slim writing into a directory the
|
|
15
|
+
// plugin manager owns would be a good way to corrupt an install.
|
|
16
|
+
//
|
|
17
|
+
// NETWORK: this is the only outbound request claude-slim makes, and it is never
|
|
18
|
+
// issued by `scan`/`clean`. It runs when the user explicitly asks (`doctor`,
|
|
19
|
+
// `check-update`), fails open on any error, and caches for a day so repeated
|
|
20
|
+
// invocations do not hammer the registry.
|
|
21
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/claude-slim/latest';
|
|
22
|
+
const FETCH_TIMEOUT_MS = 2500;
|
|
23
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
24
|
+
function getCachePath() {
|
|
25
|
+
return join(getClaudeDir(), '.claude-slim-update-check.json');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Compare dotted numeric versions. Returns >0 if a is newer, <0 if b is newer.
|
|
29
|
+
* Pre-release suffixes (`-beta.1`) sort below the same release, matching semver
|
|
30
|
+
* closely enough for "is there something newer" without pulling in a dep.
|
|
31
|
+
*/
|
|
32
|
+
export function compareVersions(a, b) {
|
|
33
|
+
const split = (v) => {
|
|
34
|
+
const [core, ...rest] = v.trim().replace(/^v/, '').split('-');
|
|
35
|
+
return [core.split('.').map((n) => Number.parseInt(n, 10) || 0), rest.join('-')];
|
|
36
|
+
};
|
|
37
|
+
const [aNums, aPre] = split(a);
|
|
38
|
+
const [bNums, bPre] = split(b);
|
|
39
|
+
for (let i = 0; i < Math.max(aNums.length, bNums.length); i++) {
|
|
40
|
+
const diff = (aNums[i] ?? 0) - (bNums[i] ?? 0);
|
|
41
|
+
if (diff !== 0)
|
|
42
|
+
return diff;
|
|
43
|
+
}
|
|
44
|
+
if (aPre === bPre)
|
|
45
|
+
return 0;
|
|
46
|
+
if (!aPre)
|
|
47
|
+
return 1;
|
|
48
|
+
if (!bPre)
|
|
49
|
+
return -1;
|
|
50
|
+
return aPre < bPre ? -1 : 1;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Infer how this copy was installed from where it sits on disk, so the hint we
|
|
54
|
+
* print is the command that will actually work for this user.
|
|
55
|
+
*/
|
|
56
|
+
export function detectInstallMethod(modulePath) {
|
|
57
|
+
const p = modulePath.replace(/\\/g, '/');
|
|
58
|
+
if (p.includes('/.claude/plugins/'))
|
|
59
|
+
return 'plugin';
|
|
60
|
+
if (p.includes('/_npx/'))
|
|
61
|
+
return 'npx';
|
|
62
|
+
if (/\/(lib\/)?node_modules\/claude-slim\//.test(p))
|
|
63
|
+
return 'global';
|
|
64
|
+
if (p.includes('/dist/') || p.includes('/src/'))
|
|
65
|
+
return 'source';
|
|
66
|
+
return 'unknown';
|
|
67
|
+
}
|
|
68
|
+
export function upgradeCommandFor(method) {
|
|
69
|
+
switch (method) {
|
|
70
|
+
case 'plugin':
|
|
71
|
+
// `claude plugin update <name>` resolves plugin@marketplace ids; the bare
|
|
72
|
+
// name fails when the marketplace shares the plugin's name.
|
|
73
|
+
return 'claude plugin marketplace update claude-slim && claude plugin update claude-slim@claude-slim';
|
|
74
|
+
case 'global':
|
|
75
|
+
return 'npm install -g claude-slim@latest';
|
|
76
|
+
case 'npx':
|
|
77
|
+
// npx resolves latest per invocation, but a cached older copy can stick.
|
|
78
|
+
return 'npx claude-slim@latest';
|
|
79
|
+
case 'source':
|
|
80
|
+
return 'git pull && npm install';
|
|
81
|
+
default:
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function getInstalledVersion() {
|
|
86
|
+
try {
|
|
87
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
88
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
89
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return '0.0.0';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function fetchLatestFromRegistry() {
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch(REGISTRY_URL, {
|
|
98
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
99
|
+
headers: { accept: 'application/json' },
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok)
|
|
102
|
+
return null;
|
|
103
|
+
const body = (await res.json());
|
|
104
|
+
return typeof body.version === 'string' ? body.version : null;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Offline, DNS failure, timeout, proxy, malformed JSON — all fail open.
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function readCache(path, now, ttlMs) {
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(await readFile(path, 'utf-8'));
|
|
114
|
+
if (parsed.version !== 1)
|
|
115
|
+
return undefined;
|
|
116
|
+
if (now - parsed.checkedAt > ttlMs)
|
|
117
|
+
return undefined;
|
|
118
|
+
return parsed.latest;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function writeCache(path, now, latest) {
|
|
125
|
+
const tmp = `${path}.tmp`;
|
|
126
|
+
try {
|
|
127
|
+
await mkdir(dirname(path), { recursive: true });
|
|
128
|
+
const body = { version: 1, checkedAt: now, latest };
|
|
129
|
+
await writeFile(tmp, JSON.stringify(body));
|
|
130
|
+
await rename(tmp, path);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// A cache we cannot persist just means the next run checks again.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export async function checkForUpdate(opts = {}) {
|
|
137
|
+
const installed = opts.installed ?? getInstalledVersion();
|
|
138
|
+
const modulePath = opts.modulePath ?? fileURLToPath(import.meta.url);
|
|
139
|
+
const installMethod = detectInstallMethod(modulePath);
|
|
140
|
+
const cachePath = opts.cachePath ?? getCachePath();
|
|
141
|
+
const now = opts.now ?? Date.now();
|
|
142
|
+
const ttlMs = opts.ttlMs ?? CACHE_TTL_MS;
|
|
143
|
+
const fetchLatest = opts.fetchLatest ?? fetchLatestFromRegistry;
|
|
144
|
+
let latest;
|
|
145
|
+
let fromCache = false;
|
|
146
|
+
if (!opts.force) {
|
|
147
|
+
latest = await readCache(cachePath, now, ttlMs);
|
|
148
|
+
fromCache = latest !== undefined;
|
|
149
|
+
}
|
|
150
|
+
if (latest === undefined) {
|
|
151
|
+
// Defensive at the boundary: the built-in lookup already fails open, but a
|
|
152
|
+
// version check must never be able to take down `doctor`, whatever the
|
|
153
|
+
// injected fetcher does.
|
|
154
|
+
try {
|
|
155
|
+
latest = await fetchLatest();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
latest = null;
|
|
159
|
+
}
|
|
160
|
+
await writeCache(cachePath, now, latest);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
installed,
|
|
164
|
+
latest: latest ?? null,
|
|
165
|
+
outdated: latest != null && compareVersions(latest, installed) > 0,
|
|
166
|
+
installMethod,
|
|
167
|
+
upgradeCommand: upgradeCommandFor(installMethod),
|
|
168
|
+
fromCache,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** One-line human summary; null when there is nothing worth saying. */
|
|
172
|
+
export function formatUpdateNotice(result) {
|
|
173
|
+
if (!result.outdated || result.latest === null)
|
|
174
|
+
return null;
|
|
175
|
+
const cmd = result.upgradeCommand ? `\n → ${result.upgradeCommand}` : '';
|
|
176
|
+
return `claude-slim ${result.installed} is installed; ${result.latest} is available.${cmd}`;
|
|
177
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-slim",
|
|
3
|
-
"version": "2.
|
|
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": {
|
|
@@ -13,10 +13,38 @@ Analyze the user's Claude Code environment for token waste and perform non-destr
|
|
|
13
13
|
- `/claude-slim scan` → report only, no changes
|
|
14
14
|
- `/claude-slim scan --json` → raw JSON output
|
|
15
15
|
- `/claude-slim doctor` → check scanner prerequisites and session-log signal quality
|
|
16
|
+
- `/claude-slim check-update` → report whether a newer version is published
|
|
16
17
|
- `/claude-slim restore` → restore previously disabled items
|
|
17
18
|
|
|
18
19
|
---
|
|
19
20
|
|
|
21
|
+
## Phase 0 — Version gate (run before every scan)
|
|
22
|
+
|
|
23
|
+
An outdated claude-slim does not merely lack features — it reports **wrong numbers**. Versions before 2.8.0 summed memory across every project on disk and inflated the startup estimate roughly 8×. Presenting those figures as fact is worse than not running at all, so check first:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js check-update --json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The check is cached for 24h and fails open — if it errors, times out, or returns `"latest": null`, **proceed silently**. Never block the user because a version lookup failed.
|
|
30
|
+
|
|
31
|
+
If `"outdated": true`, stop and tell the user before scanning:
|
|
32
|
+
|
|
33
|
+
> 설치된 claude-slim이 {installed}이고 최신은 {latest}입니다.
|
|
34
|
+
> 2.8.0 이전 버전은 시작 토큰을 약 8배 부풀려 보고합니다 — 지금 스캔하면 그 숫자가 나옵니다.
|
|
35
|
+
>
|
|
36
|
+
> {upgradeCommand}
|
|
37
|
+
>
|
|
38
|
+
> 업데이트 후 진행할까요, 아니면 현재 버전으로 계속할까요?
|
|
39
|
+
|
|
40
|
+
Then honour their answer. If they choose to continue, run the scan but **label the numbers as coming from an outdated version** in your report.
|
|
41
|
+
|
|
42
|
+
**Plugin installs need a restart.** `claude plugin update` writes the new version to disk, but the running session keeps the loaded copy. Tell the user this explicitly — otherwise they update, re-run, and see the same stale numbers with no idea why.
|
|
43
|
+
|
|
44
|
+
If `"outdated": false`, say nothing and continue to Phase 1.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
20
48
|
## Phase 1 — Scan
|
|
21
49
|
|
|
22
50
|
Run the CLI to collect environment data:
|