claude-slim 2.8.0 → 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 +38 -9
- package/dist/cli.js +28 -0
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +32 -0
- package/dist/tokenizer.js +12 -7
- 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
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
# claude-slim
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/claude-slim)
|
|
6
|
+
[](https://www.npmjs.com/package/claude-slim)
|
|
6
7
|
[](https://github.com/iops-leo/claude-slim/actions/workflows/ci.yml)
|
|
8
|
+
[](https://nodejs.org)
|
|
7
9
|
[](./LICENSE)
|
|
8
10
|
|
|
9
11
|
**Your Claude Code session burns thousands of tokens before you even say "hello."**
|
|
@@ -28,17 +30,20 @@ No proxy, no compression, no changes to how Claude Code talks to the API — it
|
|
|
28
30
|
<img src="docs/demo.gif" alt="claude-slim cleanup: 11,442 tokens of overhead reduced to ~5,800 in 45 seconds" width="900" />
|
|
29
31
|
</p>
|
|
30
32
|
|
|
31
|
-
Where the bloat hides:
|
|
33
|
+
Where the bloat hides — measured on one real install:
|
|
32
34
|
|
|
33
|
-
| Source |
|
|
34
|
-
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
| Deferred tools list | ~1,500 tokens |
|
|
39
|
-
|
|
|
35
|
+
| Source | What it costs | |
|
|
36
|
+
|--------|:---:|---|
|
|
37
|
+
| Skill listings | ~10,100 tokens | 256 skills × their `name: description` line |
|
|
38
|
+
| Agent catalog | ~2,250 tokens | `~/.claude/agents/`, 12 agents |
|
|
39
|
+
| CLAUDE.md | ~2,000 tokens | plugin instructions |
|
|
40
|
+
| Deferred tools list | ~1,500 tokens | MCP tool schemas |
|
|
41
|
+
| Slash commands | ~80 tokens | `~/.claude/commands/` |
|
|
42
|
+
| Memory files | **0 – 63,500 tokens** | current project only — varies wildly per project |
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
Skill listings are the part people underestimate: each installed skill contributes one `- name: description` line to the system prompt, and those run anywhere from **30 to 509 tokens each**. Sixty terse skills and sixty verbose ones are not the same bill.
|
|
45
|
+
|
|
46
|
+
That's slower responses. Hitting your usage cap faster. Paying for context you're not using — on every turn, of every session.
|
|
42
47
|
|
|
43
48
|
---
|
|
44
49
|
|
|
@@ -144,6 +149,7 @@ Then just type `/claude-slim` in any session.
|
|
|
144
149
|
/claude-slim scan --json # Machine-readable JSON output
|
|
145
150
|
/claude-slim scan --lookback-days 30 # Treat skills idle for 30+ days as unused
|
|
146
151
|
/claude-slim doctor # Check scanner prerequisites and data fidelity
|
|
152
|
+
/claude-slim check-update # Is a newer version published?
|
|
147
153
|
/claude-slim restore # Bring back anything you disabled
|
|
148
154
|
```
|
|
149
155
|
|
|
@@ -156,10 +162,26 @@ npx claude-slim clean --auto # Non-interactive, Tier 1 only (CI/scri
|
|
|
156
162
|
npx claude-slim clean --lookback-days N # Tune the unused-skill detection window
|
|
157
163
|
npx claude-slim scan # Report only
|
|
158
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
|
|
159
167
|
npx claude-slim restore # Undo
|
|
160
168
|
npx claude-slim report # Show savings from last clean
|
|
161
169
|
```
|
|
162
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
|
+
|
|
163
185
|
---
|
|
164
186
|
|
|
165
187
|
## Safety first
|
|
@@ -176,6 +198,7 @@ npx claude-slim report # Show savings from last clean
|
|
|
176
198
|
|
|
177
199
|
- **`~/.claude/CLAUDE.md`** — your system instructions, read-only.
|
|
178
200
|
- **`~/.claude/settings.json`** — MCP server config, hooks, and any other settings. Read-only.
|
|
201
|
+
- **`~/.claude/agents/` and `~/.claude/commands/`** — measured and reported since v2.8, never moved or deleted. There is no restore path for them yet, and a destructive action without its undo isn't worth shipping.
|
|
179
202
|
- **Plugin internals** (`~/.claude/plugins/config.json`, individual `plugin.json` files) — left alone; use `claude plugin` to manage plugins.
|
|
180
203
|
- **Git / project sources** — claude-slim only looks inside `~/.claude/`, never at your code.
|
|
181
204
|
- **Anything outside `~/.claude/`** — a path-containment guard refuses destructive ops anywhere else, even if a tampered manifest asked it to.
|
|
@@ -214,6 +237,12 @@ From a real cleanup session:
|
|
|
214
237
|
| Memory files | 15KB | 2KB | **-87%** |
|
|
215
238
|
| **Est. token savings** | | **~4,300/session** | |
|
|
216
239
|
|
|
240
|
+
### A note on the numbers
|
|
241
|
+
|
|
242
|
+
claude-slim reports what a session in **this** directory pays. Memory is per-project — Claude Code loads `~/.claude/projects/<slug>/memory/` for the project you're in, not every project on disk — so running `scan` from two different repos will legitimately give you two different totals.
|
|
243
|
+
|
|
244
|
+
Token counts come from [js-tiktoken](https://github.com/nicolo-ribaudo/js-tiktoken) against the actual file contents. The only estimates left are marked with `~`: MCP tool schemas (~8 tokens/tool) and skills whose frontmatter can't be parsed (~30 tokens). Everything else is measured.
|
|
245
|
+
|
|
217
246
|
---
|
|
218
247
|
|
|
219
248
|
## v2.8.0 — What's new
|
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) {
|
package/dist/tokenizer.js
CHANGED
|
@@ -11,13 +11,18 @@ function getCachePath() {
|
|
|
11
11
|
let cache = { version: 1, entries: {} };
|
|
12
12
|
let cacheDirty = false;
|
|
13
13
|
export async function initTokenizer() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
// Building the cl100k_base encoder parses a large rank table — hundreds of
|
|
15
|
+
// milliseconds, and far more under CPU contention. It is immutable once
|
|
16
|
+
// built, so reuse it across calls. Only the cache is per-init state.
|
|
17
|
+
if (encoder === null && !useFallback) {
|
|
18
|
+
try {
|
|
19
|
+
// cl100k_base is closest to Claude's tokenizer (gpt-4o uses o200k_base which undercounts by ~15%)
|
|
20
|
+
const { getEncoding } = await import('js-tiktoken');
|
|
21
|
+
encoder = getEncoding('cl100k_base');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
useFallback = true;
|
|
25
|
+
}
|
|
21
26
|
}
|
|
22
27
|
// Reset in-memory state so repeated initTokenizer() calls (e.g. across
|
|
23
28
|
// test cases) don't bleed cache entries from a prior invocation.
|
|
@@ -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.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:
|