claude-slim 2.8.1 → 2.9.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 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
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')
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) {
@@ -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.8.1",
3
+ "version": "2.9.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": {
@@ -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: