claude-slim 2.11.0 → 2.12.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
@@ -165,6 +165,7 @@ npx claude-slim scan --no-codex # Skip the ~/.codex scan
165
165
  npx claude-slim doctor # Diagnose Node/Claude/session-log readiness
166
166
  npx claude-slim doctor --offline # Same, without the version check (no network)
167
167
  npx claude-slim check-update # Report-only version check
168
+ npx claude-slim update # Run the upgrade for this install method
168
169
  npx claude-slim restore # Undo
169
170
  npx claude-slim report # Show savings from last clean
170
171
  ```
@@ -247,16 +248,12 @@ Token counts come from [js-tiktoken](https://github.com/nicolo-ribaudo/js-tiktok
247
248
 
248
249
  ---
249
250
 
250
- ## v2.11.0 — What's new
251
+ ## v2.12.1 — What's new
251
252
 
252
- Codex gets the same tiered propose-and-choose cleanup Claude Code has, minus the two categories Codex cannot honestly support.
253
+ - **Fixed: the `/claude-slim` skill reported project memory as zero.** It invoked the CLI with `cd "${CLAUDE_PLUGIN_ROOT}"`, making `cwd` the plugin cache directory; the project slug resolved there, matched nothing, and every project-memory token silently left the startup total — **108,570** on the machine where this surfaced. `SKILL.md` no longer `cd`s. This hit the tool's primary entry point, and quietly: a zero reads as a clean result.
254
+ - **`--project-dir <path>`** for callers that cannot run from the project directory, plus a warning when the CLI notices it is running from its own install without one.
253
255
 
254
- - **Codex cleanup, tiered.** Five of seven categories carry over because they are filesystem facts, not usage inferences: broken symlinks and unfilled templates (Tier 1), install leftovers in `~/.codex/.tmp` (Tier 1 — 146MB on the dev machine), backup copies and plugin-shadowed duplicates (Tier 2), oversized skills (Tier 3). Codex items join the same numbered list tagged `[codex]`. `--no-codex` opts out.
255
- - **`~/.codex/skills.disabled/`** — moves are reversible through the same `restore`, and manifest entries record which agent they came from.
256
- - **The path guard is now per-agent.** Not "inside any known root" — a Codex issue must not resolve into `~/.claude/` and vice versa, so a tampered manifest cannot cross between agents.
257
- - `unused_skill` and `oversized_memory` remain unavailable for Codex: no invocation record, and no `~/.codex/projects/*/memory/`.
258
-
259
- Tests: 347 → 374 (+27), the guard suite being the point — cross-agent isolation both directions, traversal escapes, and a `~/.claude-backup` sibling a naive `startsWith` would have allowed.
256
+ Tests: 392 401 (+9).
260
257
 
261
258
  For older release notes, see [CHANGELOG.md](CHANGELOG.md).
262
259
 
package/dist/cli.js CHANGED
@@ -10,7 +10,9 @@ 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 { looksLikeToolInstallDir } from './paths.js';
13
14
  import { checkForUpdate, formatUpdateNotice } from './update-check.js';
15
+ import { confirmDecision, planUpdate, renderStep, runUpdate } from './update-run.js';
14
16
  import { scanCodex } from './codex/index.js';
15
17
  import { formatCodexSummary } from './codex/report.js';
16
18
  import { classifyCodexIssues } from './codex/detectors.js';
@@ -39,10 +41,15 @@ program
39
41
  .description('Scan environment and report issues')
40
42
  .option('--json', 'Output raw JSON')
41
43
  .option('--lookback-days <n>', 'Days of session history for skill-usage analysis', '60')
44
+ .option('--project-dir <path>', 'Directory whose project memory counts toward the total (default: cwd)')
42
45
  .option('--no-codex', 'Skip the ~/.codex scan even if Codex is installed')
43
46
  .action(async (opts) => {
44
47
  await initTokenizer();
45
- const result = await scan({ lookbackDays: parseNonNegativeInt(opts.lookbackDays, 60) });
48
+ const projectDir = resolveProjectDir(opts.projectDir);
49
+ const result = await scan({
50
+ lookbackDays: parseNonNegativeInt(opts.lookbackDays, 60),
51
+ projectDir,
52
+ });
46
53
  // Codex is scanned when present. Reported only — it is never modified, and
47
54
  // unused-skill detection is suppressed there for lack of a usage signal.
48
55
  // commander maps `--no-codex` to `opts.codex === false`, not `opts.noCodex`.
@@ -101,6 +108,81 @@ program
101
108
  console.log(`\n \x1b[32m✓\x1b[0m claude-slim ${result.installed} is up to date.\n`);
102
109
  }
103
110
  });
111
+ // --- update ---
112
+ // Detection lives in `check-update`; this runs the command that detection
113
+ // identified. claude-slim still never writes into a package manager's
114
+ // directories itself — it invokes the manager, the same way cleanup already
115
+ // invokes `claude plugin disable`.
116
+ program
117
+ .command('update')
118
+ .description('Run the upgrade command for however this copy was installed')
119
+ .option('--yes', 'Skip the confirmation prompt')
120
+ .option('--dry-run', 'Show the commands without running them')
121
+ .action(async (opts) => {
122
+ const result = await checkForUpdate({ force: true });
123
+ const plan = planUpdate(result.installMethod);
124
+ console.log('');
125
+ console.log(` Installed: ${result.installed} Latest: ${result.latest ?? 'unknown'}`);
126
+ console.log(` Install method: ${result.installMethod}`);
127
+ console.log('');
128
+ if (result.latest === null) {
129
+ console.log(' Could not reach the npm registry, so there is nothing to compare against.');
130
+ console.log('');
131
+ process.exitCode = 1;
132
+ return;
133
+ }
134
+ if (!result.outdated) {
135
+ console.log(' \x1b[32m✓\x1b[0m Already up to date.\n');
136
+ return;
137
+ }
138
+ if (!plan.runnable) {
139
+ console.log(` ${plan.guidance}\n`);
140
+ return;
141
+ }
142
+ console.log(' Will run:');
143
+ for (const step of plan.steps)
144
+ console.log(` ${renderStep(step)}`);
145
+ console.log('');
146
+ if (opts.dryRun) {
147
+ console.log(' \x1b[90mDry run — nothing was executed.\x1b[0m\n');
148
+ return;
149
+ }
150
+ const gate = confirmDecision(opts, Boolean(process.stdin.isTTY));
151
+ if (gate === 'refuse') {
152
+ console.error(' Refusing to run unattended. Re-run with --yes to confirm.\n');
153
+ process.exitCode = 1;
154
+ return;
155
+ }
156
+ if (gate === 'prompt') {
157
+ const answer = await askUser(' Proceed? [y/N] ');
158
+ if (!/^y(es)?$/i.test(answer)) {
159
+ console.log(' Cancelled.\n');
160
+ return;
161
+ }
162
+ console.log('');
163
+ }
164
+ const results = await runUpdate(plan);
165
+ for (const r of results) {
166
+ const mark = r.ok ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
167
+ console.log(` ${mark} ${renderStep(r.step)}`);
168
+ if (r.output)
169
+ console.log(r.output.split('\n').map((l) => ` ${l}`).join('\n'));
170
+ }
171
+ const failed = results.find((r) => !r.ok);
172
+ console.log('');
173
+ if (failed) {
174
+ console.log(' Update did not complete. Run the commands above manually to see the full error.\n');
175
+ process.exitCode = 1;
176
+ }
177
+ else if (result.installMethod === 'plugin') {
178
+ // The new version is on disk but the running session still holds the old
179
+ // one — without saying so, users update, re-run, and see identical output.
180
+ console.log(' \x1b[33m!\x1b[0m Restart Claude Code for the update to take effect.\n');
181
+ }
182
+ else {
183
+ console.log(' \x1b[32m✓\x1b[0m Updated.\n');
184
+ }
185
+ });
104
186
  // --- clean ---
105
187
  program
106
188
  .command('clean')
@@ -109,6 +191,7 @@ program
109
191
  .option('--auto', 'Non-interactive: auto-select Tier 1 items only')
110
192
  .option('--sessions-per-day <n>', 'Sessions per day for savings estimate', '2')
111
193
  .option('--lookback-days <n>', 'Days of session history for skill-usage analysis', '60')
194
+ .option('--project-dir <path>', 'Directory whose project memory counts toward the total (default: cwd)')
112
195
  .option('--no-codex', 'Skip ~/.codex entirely')
113
196
  .action(async (opts) => {
114
197
  await runCleanPipeline({
@@ -117,6 +200,7 @@ program
117
200
  sessionsPerDay: parseNonNegativeInt(opts.sessionsPerDay, 2),
118
201
  lookbackDays: parseNonNegativeInt(opts.lookbackDays, 60),
119
202
  codex: opts.codex !== false,
203
+ projectDir: opts.projectDir,
120
204
  });
121
205
  });
122
206
  // --- restore ---
@@ -274,10 +358,28 @@ program.action(async () => {
274
358
  }
275
359
  await runCleanPipeline({ dryRun: false, auto: false, sessionsPerDay: 2, lookbackDays: 60, codex: true });
276
360
  });
361
+ /**
362
+ * Resolve which directory's project memory counts toward the startup estimate.
363
+ *
364
+ * Warns when the CLI is running from its own install directory and no explicit
365
+ * directory was given: the slug would resolve to the plugin cache, match no
366
+ * project, and silently zero out every project-memory token. Warning beats
367
+ * guessing — we cannot know which project the user meant.
368
+ */
369
+ function resolveProjectDir(explicit) {
370
+ if (explicit)
371
+ return explicit;
372
+ if (looksLikeToolInstallDir()) {
373
+ console.error(' \x1b[33m!\x1b[0m Running from claude-slim\'s own install directory, so project memory\n' +
374
+ ' cannot be attributed and is reported as 0. Pass --project-dir <path>\n' +
375
+ ' (or run from your project) for an accurate startup total.\n');
376
+ }
377
+ return undefined;
378
+ }
277
379
  // --- shared clean pipeline ---
278
380
  async function runCleanPipeline(opts) {
279
381
  await initTokenizer();
280
- const result = await scan({ lookbackDays: opts.lookbackDays });
382
+ const result = await scan({ lookbackDays: opts.lookbackDays, projectDir: resolveProjectDir(opts.projectDir) });
281
383
  // Codex issues join the same tiered list. They carry `agent: 'codex'`, which
282
384
  // is what keeps the cleaner's path guard pointed at ~/.codex/.
283
385
  const codexContents = new Map();
package/dist/paths.d.ts CHANGED
@@ -12,6 +12,17 @@ export declare function getDisabledDir(): string;
12
12
  * the startup estimate must not sum memory across every project on disk.
13
13
  */
14
14
  export declare function getCurrentProjectSlug(cwd?: string): string;
15
+ /**
16
+ * True when `cwd` sits inside claude-slim's own install rather than a project.
17
+ *
18
+ * The `/claude-slim` skill invokes the CLI with `cd "${CLAUDE_PLUGIN_ROOT}"`,
19
+ * which makes `process.cwd()` the plugin cache directory. The project slug then
20
+ * resolves to that path, no memory matches it, and the startup estimate silently
21
+ * drops every project-memory token — 108,570 of them on the machine where this
22
+ * was found. Detecting it lets the caller fail loudly or be told to pass
23
+ * `--project-dir` instead of quietly reporting zero.
24
+ */
25
+ export declare function looksLikeToolInstallDir(cwd?: string): boolean;
15
26
  export declare function getManifestPath(): string;
16
27
  export declare function getLegacyManifestPath(): string;
17
28
  /** The agents claude-slim is allowed to touch. Adding one widens what every
package/dist/paths.js CHANGED
@@ -26,6 +26,22 @@ export function getDisabledDir() {
26
26
  export function getCurrentProjectSlug(cwd = process.cwd()) {
27
27
  return resolve(cwd).replace(/\//g, '-');
28
28
  }
29
+ /**
30
+ * True when `cwd` sits inside claude-slim's own install rather than a project.
31
+ *
32
+ * The `/claude-slim` skill invokes the CLI with `cd "${CLAUDE_PLUGIN_ROOT}"`,
33
+ * which makes `process.cwd()` the plugin cache directory. The project slug then
34
+ * resolves to that path, no memory matches it, and the startup estimate silently
35
+ * drops every project-memory token — 108,570 of them on the machine where this
36
+ * was found. Detecting it lets the caller fail loudly or be told to pass
37
+ * `--project-dir` instead of quietly reporting zero.
38
+ */
39
+ export function looksLikeToolInstallDir(cwd = process.cwd()) {
40
+ const p = resolve(cwd).replace(/\\/g, '/');
41
+ return (p.includes('/.claude/plugins/') ||
42
+ p.includes('/_npx/') ||
43
+ /\/node_modules\/claude-slim(\/|$)/.test(p));
44
+ }
29
45
  export function getManifestPath() {
30
46
  return join(getDisabledDir(), 'manifest.json');
31
47
  }
@@ -1,5 +1,14 @@
1
1
  import type { ScanResult } from '../types.js';
2
2
  export interface ScanOptions {
3
3
  lookbackDays?: number;
4
+ /**
5
+ * Directory whose project memory counts toward the startup estimate.
6
+ *
7
+ * Defaults to `process.cwd()`, which is wrong whenever the CLI is launched
8
+ * from its own install directory — the `/claude-slim` skill does exactly that
9
+ * via `cd "${CLAUDE_PLUGIN_ROOT}"`, and every project-memory token silently
10
+ * dropped out of the total as a result.
11
+ */
12
+ projectDir?: string;
4
13
  }
5
14
  export declare function scan(opts?: ScanOptions): Promise<ScanResult>;
@@ -97,7 +97,7 @@ export async function scan(opts = {}) {
97
97
  // disk. Summing all of them (pre-2.8 behaviour) inflated the startup estimate
98
98
  // by a factor of however many projects the user had — 100k+ tokens on a busy
99
99
  // machine, for a number labelled "tokens at session start".
100
- const currentProjectSlug = getCurrentProjectSlug();
100
+ const currentProjectSlug = getCurrentProjectSlug(opts.projectDir);
101
101
  const currentProjectMemoryTokens = memoryFiles
102
102
  .filter((m) => m.project === currentProjectSlug)
103
103
  .reduce((sum, m) => sum + m.tokens, 0);
@@ -0,0 +1,40 @@
1
+ import type { InstallMethod } from './update-check.js';
2
+ export interface UpdateStep {
3
+ file: string;
4
+ args: string[];
5
+ }
6
+ export interface UpdatePlan {
7
+ /** false when there is nothing to execute — see `guidance`. */
8
+ runnable: boolean;
9
+ steps: UpdateStep[];
10
+ /** Why nothing runs, for the methods where that is the correct answer. */
11
+ guidance?: string;
12
+ }
13
+ export declare function planUpdate(method: InstallMethod): UpdatePlan;
14
+ /** Human-readable rendering of a step, matching what the user would type. */
15
+ export declare function renderStep(step: UpdateStep): string;
16
+ export interface StepResult {
17
+ step: UpdateStep;
18
+ ok: boolean;
19
+ output: string;
20
+ }
21
+ /**
22
+ * Run the plan's steps in order, stopping at the first failure.
23
+ *
24
+ * Sequential rather than parallel, and halting on error, because step 2 depends
25
+ * on step 1 having refreshed the marketplace — running it against a stale
26
+ * manifest would report success while changing nothing.
27
+ */
28
+ export declare function runUpdate(plan: UpdatePlan): Promise<StepResult[]>;
29
+ export type ConfirmDecision = 'run' | 'prompt' | 'refuse';
30
+ /**
31
+ * Decide how to gate an update that will modify the user's install.
32
+ *
33
+ * Refusing when there is no TTY is the important case: piped into a script or a
34
+ * CI job, there is nobody to answer the prompt, and silently proceeding would
35
+ * mean a tool that changes an installation without anyone agreeing to it.
36
+ * `--yes` is how you say so deliberately.
37
+ */
38
+ export declare function confirmDecision(opts: {
39
+ yes?: boolean;
40
+ }, isTTY: boolean): ConfirmDecision;
@@ -0,0 +1,101 @@
1
+ import { execFile } from 'node:child_process';
2
+ // Running the upgrade.
3
+ //
4
+ // v2.9.0 shipped detection only, on the reasoning that updating is the package
5
+ // manager's job. Half of that was right and half was not. Writing into the
6
+ // plugin directory ourselves would indeed corrupt an install — but *invoking*
7
+ // the package manager is something this tool already does elsewhere
8
+ // (`claude plugin disable` during unused-plugin cleanup). Refusing to invoke it
9
+ // here was inconsistent, not principled.
10
+ //
11
+ // So: still never write to those directories directly. Just run the command the
12
+ // user would have typed, after showing it to them.
13
+ //
14
+ // Every argv below is a fixed literal. Nothing from the environment, the
15
+ // manifest, or user input is interpolated, and execFile never routes through a
16
+ // shell — so there is no argument-injection surface to guard.
17
+ const STEP_TIMEOUT_MS = 180_000;
18
+ export function planUpdate(method) {
19
+ switch (method) {
20
+ case 'plugin':
21
+ return {
22
+ runnable: true,
23
+ steps: [
24
+ // Refresh the marketplace first; otherwise `plugin update` compares
25
+ // against a stale manifest and reports "already up to date".
26
+ { file: 'claude', args: ['plugin', 'marketplace', 'update', 'claude-slim'] },
27
+ // The qualified id is required: the bare name fails with
28
+ // `Plugin "claude-slim" not found` when a marketplace shares its name.
29
+ { file: 'claude', args: ['plugin', 'update', 'claude-slim@claude-slim'] },
30
+ ],
31
+ };
32
+ case 'global':
33
+ return {
34
+ runnable: true,
35
+ steps: [{ file: 'npm', args: ['install', '-g', 'claude-slim@latest'] }],
36
+ };
37
+ case 'npx':
38
+ return {
39
+ runnable: false,
40
+ steps: [],
41
+ guidance: 'npx resolves the latest version on every invocation, so there is nothing to update. ' +
42
+ 'If a stale copy is cached, run `npx claude-slim@latest` once to refresh it.',
43
+ };
44
+ case 'source':
45
+ return {
46
+ runnable: false,
47
+ steps: [],
48
+ guidance: 'This is a source checkout. Updating it means pulling your own repository, ' +
49
+ 'which claude-slim will not do on your behalf — run `git pull && npm install` yourself.',
50
+ };
51
+ default:
52
+ return {
53
+ runnable: false,
54
+ steps: [],
55
+ guidance: 'Could not tell how this copy was installed, so no upgrade command can be chosen safely. ' +
56
+ 'Update it the same way you installed it.',
57
+ };
58
+ }
59
+ }
60
+ /** Human-readable rendering of a step, matching what the user would type. */
61
+ export function renderStep(step) {
62
+ return [step.file, ...step.args].join(' ');
63
+ }
64
+ function runStep(step) {
65
+ return new Promise((resolve) => {
66
+ execFile(step.file, step.args, { timeout: STEP_TIMEOUT_MS }, (err, stdout, stderr) => {
67
+ const output = [stdout, stderr].map((s) => s?.trim()).filter(Boolean).join('\n');
68
+ resolve({ step, ok: !err, output: err ? `${err.message}${output ? `\n${output}` : ''}` : output });
69
+ });
70
+ });
71
+ }
72
+ /**
73
+ * Run the plan's steps in order, stopping at the first failure.
74
+ *
75
+ * Sequential rather than parallel, and halting on error, because step 2 depends
76
+ * on step 1 having refreshed the marketplace — running it against a stale
77
+ * manifest would report success while changing nothing.
78
+ */
79
+ export async function runUpdate(plan) {
80
+ const results = [];
81
+ for (const step of plan.steps) {
82
+ const result = await runStep(step);
83
+ results.push(result);
84
+ if (!result.ok)
85
+ break;
86
+ }
87
+ return results;
88
+ }
89
+ /**
90
+ * Decide how to gate an update that will modify the user's install.
91
+ *
92
+ * Refusing when there is no TTY is the important case: piped into a script or a
93
+ * CI job, there is nobody to answer the prompt, and silently proceeding would
94
+ * mean a tool that changes an installation without anyone agreeing to it.
95
+ * `--yes` is how you say so deliberately.
96
+ */
97
+ export function confirmDecision(opts, isTTY) {
98
+ if (opts.yes)
99
+ return 'run';
100
+ return isTTY ? 'prompt' : 'refuse';
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-slim",
3
- "version": "2.11.0",
3
+ "version": "2.12.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": {
@@ -23,7 +23,7 @@ Analyze the user's Claude Code environment for token waste and perform non-destr
23
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
24
 
25
25
  ```bash
26
- cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js check-update --json
26
+ node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" check-update --json
27
27
  ```
28
28
 
29
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.
@@ -50,13 +50,13 @@ If `"outdated": false`, say nothing and continue to Phase 1.
50
50
  Run the CLI to collect environment data:
51
51
 
52
52
  ```bash
53
- cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js scan --json
53
+ node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" scan --json
54
54
  ```
55
55
 
56
56
  If `CLAUDE_PLUGIN_ROOT` is not set:
57
57
  ```bash
58
58
  PLUGIN_DIR=$(find ~/.claude/plugins -path "*/claude-slim/dist/cli.js" -type f 2>/dev/null | head -1 | xargs dirname | xargs dirname)
59
- cd "$PLUGIN_DIR" && node dist/cli.js scan --json
59
+ node "$PLUGIN_DIR/dist/cli.js" scan --json
60
60
  ```
61
61
 
62
62
  If `node` is not available, fall back to the legacy bash scanner:
@@ -134,18 +134,18 @@ If subcommand is `scan`, stop here. Ask a localized equivalent of "Proceed with
134
134
  Run the interactive clean command:
135
135
 
136
136
  ```bash
137
- cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js clean
137
+ node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" clean
138
138
  ```
139
139
 
140
140
  Or with dry-run:
141
141
  ```bash
142
- cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js clean --dry-run
142
+ node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" clean --dry-run
143
143
  ```
144
144
 
145
145
  After cleanup, re-run scan to get updated numbers, then show the savings report:
146
146
 
147
147
  ```bash
148
- cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js report
148
+ node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" report
149
149
  ```
150
150
 
151
151
  Present the report box AND the before/after breakdown table to the user.
@@ -157,7 +157,7 @@ Present the report box AND the before/after breakdown table to the user.
157
157
  When `/claude-slim restore` is invoked:
158
158
 
159
159
  ```bash
160
- cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js restore
160
+ node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" restore
161
161
  ```
162
162
 
163
163
  ## Doctor
@@ -165,7 +165,7 @@ cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js restore
165
165
  When `/claude-slim doctor` is invoked:
166
166
 
167
167
  ```bash
168
- cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js doctor
168
+ node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" doctor
169
169
  ```
170
170
 
171
171
  Explain warnings in the user's language. Pay special attention to session-log warnings because they explain why unused-skill detection may be suppressed.