claude-slim 2.2.0 → 2.2.2
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/dist/cleaner.js +18 -2
- package/dist/cli.js +26 -8
- package/dist/report.d.ts +1 -1
- package/dist/report.js +8 -9
- package/dist/scanner.d.ts +2 -0
- package/dist/scanner.js +19 -11
- package/package.json +1 -1
- package/skills/claude-slim/SKILL.md +18 -16
package/dist/cleaner.js
CHANGED
|
@@ -11,6 +11,20 @@ async function pathExists(p) {
|
|
|
11
11
|
return false;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
// Record the manifest entry; if it fails, run the caller's compensation to
|
|
15
|
+
// undo the filesystem side effect so we never leave an untracked orphan.
|
|
16
|
+
async function recordOrRollback(entry, rollback) {
|
|
17
|
+
try {
|
|
18
|
+
await appendManifest(entry);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
try {
|
|
22
|
+
await rollback();
|
|
23
|
+
}
|
|
24
|
+
catch { /* best-effort */ }
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
14
28
|
export async function cleanIssues(issues) {
|
|
15
29
|
await ensureDisabledDir();
|
|
16
30
|
const disabledDir = getDisabledDir();
|
|
@@ -29,6 +43,7 @@ export async function cleanIssues(issues) {
|
|
|
29
43
|
tokenCount: issue.tokens,
|
|
30
44
|
tier: issue.tier,
|
|
31
45
|
};
|
|
46
|
+
// unlink is not reversible; best-effort append only
|
|
32
47
|
await appendManifest(entry);
|
|
33
48
|
moved.push(entry);
|
|
34
49
|
}
|
|
@@ -48,7 +63,7 @@ export async function cleanIssues(issues) {
|
|
|
48
63
|
tokenCount: issue.tokens,
|
|
49
64
|
tier: issue.tier,
|
|
50
65
|
};
|
|
51
|
-
await
|
|
66
|
+
await recordOrRollback(entry, () => rename(dest, issue.path));
|
|
52
67
|
moved.push(entry);
|
|
53
68
|
}
|
|
54
69
|
else if (issue.type === 'temp_cache') {
|
|
@@ -62,6 +77,7 @@ export async function cleanIssues(issues) {
|
|
|
62
77
|
tokenCount: 0,
|
|
63
78
|
tier: issue.tier,
|
|
64
79
|
};
|
|
80
|
+
// rm is not reversible; best-effort append only
|
|
65
81
|
await appendManifest(entry);
|
|
66
82
|
moved.push(entry);
|
|
67
83
|
}
|
|
@@ -84,7 +100,7 @@ export async function cleanIssues(issues) {
|
|
|
84
100
|
tokenCount: issue.tokens,
|
|
85
101
|
tier: issue.tier,
|
|
86
102
|
};
|
|
87
|
-
await
|
|
103
|
+
await recordOrRollback(entry, () => rename(backupDir, issue.path));
|
|
88
104
|
moved.push(entry);
|
|
89
105
|
}
|
|
90
106
|
else if (issue.type === 'oversized_memory' || issue.type === 'disabled_plugin') {
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
2
5
|
import { createInterface } from 'node:readline';
|
|
3
6
|
import { Command } from 'commander';
|
|
4
7
|
import { initTokenizer, flushCache } from './tokenizer.js';
|
|
5
|
-
import { scan } from './scanner.js';
|
|
8
|
+
import { scan, SKILL_PROMPT_OVERHEAD_TOKENS } from './scanner.js';
|
|
6
9
|
import { cleanIssues, restoreItem } from './cleaner.js';
|
|
7
10
|
import { readManifest } from './manifest.js';
|
|
8
11
|
import { formatScanSummary, formatReportBox, calculateReport, } from './report.js';
|
|
9
12
|
import { resolveSelection, resolveRestoreSelection } from './selection.js';
|
|
13
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
14
|
+
const { version: PKG_VERSION } = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
10
15
|
const program = new Command();
|
|
11
16
|
program
|
|
12
17
|
.name('claude-slim')
|
|
13
18
|
.description('Analyze and reduce Claude Code token overhead')
|
|
14
|
-
.version(
|
|
19
|
+
.version(PKG_VERSION);
|
|
15
20
|
// --- scan ---
|
|
16
21
|
program
|
|
17
22
|
.command('scan')
|
|
@@ -116,17 +121,30 @@ program
|
|
|
116
121
|
return;
|
|
117
122
|
}
|
|
118
123
|
const sessionsPerDay = parseInt(opts.sessionsPerDay, 10) || 2;
|
|
119
|
-
// Reconstruct "before" state: current + what was removed
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
// Reconstruct "before" state: current + what was removed.
|
|
125
|
+
// Only skill-type entries contributed to the per-skill prompt overhead
|
|
126
|
+
// (stale_project restores memory tokens separately; broken_symlink/
|
|
127
|
+
// temp_cache never counted toward totalTokensBefore).
|
|
128
|
+
const SKILL_TYPES = new Set(['template', 'duplicate', 'skill_dup', 'oversized_skill']);
|
|
129
|
+
const removedSkillEntries = movedEntries.filter((e) => SKILL_TYPES.has(e.type));
|
|
130
|
+
const removedMemoryTokens = movedEntries
|
|
131
|
+
.filter((e) => e.type === 'stale_project')
|
|
132
|
+
.reduce((sum, e) => sum + (e.tokenCount || 0), 0);
|
|
133
|
+
const totalBefore = result.totalTokensBefore
|
|
134
|
+
+ removedSkillEntries.length * SKILL_PROMPT_OVERHEAD_TOKENS
|
|
135
|
+
+ removedMemoryTokens;
|
|
122
136
|
const pseudoBefore = {
|
|
123
137
|
...result,
|
|
124
138
|
totalTokensBefore: totalBefore,
|
|
125
139
|
localSkills: [
|
|
126
140
|
...result.localSkills,
|
|
127
|
-
...
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
...removedSkillEntries.map((e) => ({
|
|
142
|
+
name: e.name,
|
|
143
|
+
path: e.from,
|
|
144
|
+
sizeBytes: 0,
|
|
145
|
+
tokens: e.tokenCount || 0,
|
|
146
|
+
source: 'local',
|
|
147
|
+
})),
|
|
130
148
|
],
|
|
131
149
|
};
|
|
132
150
|
const reportData = calculateReport(pseudoBefore, result, movedEntries, sessionsPerDay);
|
package/dist/report.d.ts
CHANGED
|
@@ -18,6 +18,6 @@ export interface ReportData {
|
|
|
18
18
|
sessionsPerDay: number;
|
|
19
19
|
breakdown: BreakdownRow[];
|
|
20
20
|
}
|
|
21
|
-
export declare function calculateReport(scanBefore: ScanResult, scanAfter: ScanResult
|
|
21
|
+
export declare function calculateReport(scanBefore: ScanResult, scanAfter: ScanResult, movedEntries: ManifestEntry[], sessionsPerDay?: number): ReportData;
|
|
22
22
|
export declare function formatReportBox(data: ReportData): string;
|
|
23
23
|
export declare function formatScanSummary(result: ScanResult): string;
|
package/dist/report.js
CHANGED
|
@@ -6,7 +6,7 @@ const SESSIONS_PER_DAY_DEFAULT = 2;
|
|
|
6
6
|
const PRICE_PER_1K_TOKENS = 0.003; // Claude Sonnet input price
|
|
7
7
|
export function calculateReport(scanBefore, scanAfter, movedEntries, sessionsPerDay = SESSIONS_PER_DAY_DEFAULT) {
|
|
8
8
|
const before = scanBefore.totalTokensBefore;
|
|
9
|
-
const after = scanAfter
|
|
9
|
+
const after = scanAfter.totalTokensBefore;
|
|
10
10
|
// Use actual scan difference for accurate savings, not SKILL.md file sizes
|
|
11
11
|
const saved = before - after;
|
|
12
12
|
const percent = before > 0 ? (saved / before) * 100 : 0;
|
|
@@ -18,15 +18,11 @@ export function calculateReport(scanBefore, scanAfter, movedEntries, sessionsPer
|
|
|
18
18
|
const monthlySavings = (saved / 1000) * PRICE_PER_1K_TOKENS * sessionsPerDay * 30;
|
|
19
19
|
// Breakdown rows
|
|
20
20
|
const localBefore = scanBefore.localSkills.length;
|
|
21
|
-
const localAfter = scanAfter
|
|
21
|
+
const localAfter = scanAfter.localSkills.length;
|
|
22
22
|
const promptBefore = scanBefore.localSkills.length + scanBefore.pluginSkills.length;
|
|
23
|
-
const promptAfter = scanAfter
|
|
24
|
-
? scanAfter.localSkills.length + scanAfter.pluginSkills.length
|
|
25
|
-
: promptBefore - movedEntries.filter((e) => e.type !== 'oversized_memory').length;
|
|
23
|
+
const promptAfter = scanAfter.localSkills.length + scanAfter.pluginSkills.length;
|
|
26
24
|
const memBefore = scanBefore.memoryFiles.reduce((s, m) => s + m.sizeBytes, 0);
|
|
27
|
-
const memAfter = scanAfter
|
|
28
|
-
? scanAfter.memoryFiles.reduce((s, m) => s + m.sizeBytes, 0)
|
|
29
|
-
: memBefore;
|
|
25
|
+
const memAfter = scanAfter.memoryFiles.reduce((s, m) => s + m.sizeBytes, 0);
|
|
30
26
|
const breakdown = [
|
|
31
27
|
{
|
|
32
28
|
label: 'Local skills',
|
|
@@ -207,6 +203,8 @@ export function formatScanSummary(result) {
|
|
|
207
203
|
lines.push('');
|
|
208
204
|
const tierLabels = { 1: 'Auto', 2: 'Recommended', 3: 'Optional' };
|
|
209
205
|
const tierColors = { 1: '31', 2: '33', 3: '37' };
|
|
206
|
+
// These actions delete or unlink — they cannot be restored via `claude-slim restore`.
|
|
207
|
+
const permanentTypes = new Set(['temp_cache', 'broken_symlink']);
|
|
210
208
|
for (let i = 0; i < result.issues.length; i++) {
|
|
211
209
|
const issue = result.issues[i];
|
|
212
210
|
const selected = issue.tier === 1 ? '\u2713' : '\u25cb';
|
|
@@ -214,7 +212,8 @@ export function formatScanSummary(result) {
|
|
|
214
212
|
const color = tierColors[issue.tier] || '37';
|
|
215
213
|
const detail = issue.detail ? ` (${issue.detail})` : '';
|
|
216
214
|
const tokStr = issue.tokens > 0 ? ` ~${issue.tokens.toLocaleString()} tok` : '';
|
|
217
|
-
|
|
215
|
+
const permanent = permanentTypes.has(issue.type) ? ' \x1b[31m(permanent)\x1b[0m' : '';
|
|
216
|
+
lines.push(` ${selected} ${i + 1}. \x1b[${color}m[${tierLabel}]\x1b[0m ${issue.type}: ${issue.name}${detail}${tokStr}${permanent}`);
|
|
218
217
|
}
|
|
219
218
|
}
|
|
220
219
|
lines.push('');
|
package/dist/scanner.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ScanResult, SkillInfo } from './types.js';
|
|
2
|
+
export declare const SKILL_PROMPT_OVERHEAD_TOKENS = 30;
|
|
2
3
|
interface SkillCandidate {
|
|
3
4
|
skill: SkillInfo;
|
|
4
5
|
realMdPath: string;
|
|
5
6
|
}
|
|
6
7
|
export declare function dedupeBySymlink(candidates: SkillCandidate[]): SkillInfo[];
|
|
8
|
+
export declare function parseDisabledPlugins(output: string): Set<string>;
|
|
7
9
|
export declare function parseClaudeMdSections(content: string): Array<{
|
|
8
10
|
name: string;
|
|
9
11
|
sizeBytes: number;
|
package/dist/scanner.js
CHANGED
|
@@ -3,6 +3,9 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { countTokensCached } from './tokenizer.js';
|
|
4
4
|
import { getClaudeDir, getSkillsDir, getPluginsDir, getProjectsDir } from './paths.js';
|
|
5
5
|
const STALE_DAYS = 90;
|
|
6
|
+
const OVERSIZED_SKILL_BYTES = 10240;
|
|
7
|
+
const OVERSIZED_MEMORY_BYTES = 5120;
|
|
8
|
+
export const SKILL_PROMPT_OVERHEAD_TOKENS = 30;
|
|
6
9
|
async function safeReadFile(p) {
|
|
7
10
|
try {
|
|
8
11
|
return await readFile(p, 'utf-8');
|
|
@@ -73,7 +76,9 @@ async function getDirSize(dir) {
|
|
|
73
76
|
}
|
|
74
77
|
return total;
|
|
75
78
|
}
|
|
76
|
-
// Content cache: avoids re-reading files during classification
|
|
79
|
+
// Content cache: avoids re-reading files during classification.
|
|
80
|
+
// Reset on every scan() so repeat invocations (e.g. pre/post-cleanup) do
|
|
81
|
+
// not accumulate entries for paths that no longer exist.
|
|
77
82
|
const contentCache = new Map();
|
|
78
83
|
async function resolveRealPath(p) {
|
|
79
84
|
try {
|
|
@@ -279,11 +284,10 @@ async function scanMemoryFiles() {
|
|
|
279
284
|
await Promise.all(scanPromises);
|
|
280
285
|
return { memoryFiles, staleProjects };
|
|
281
286
|
}
|
|
282
|
-
|
|
283
|
-
const output = await runCommand('claude plugin list');
|
|
284
|
-
if (!output)
|
|
285
|
-
return new Set();
|
|
287
|
+
export function parseDisabledPlugins(output) {
|
|
286
288
|
const disabled = new Set();
|
|
289
|
+
if (!output)
|
|
290
|
+
return disabled;
|
|
287
291
|
let currentName = null;
|
|
288
292
|
for (const line of output.split('\n')) {
|
|
289
293
|
const trimmed = line.trim();
|
|
@@ -302,6 +306,9 @@ async function getDisabledPlugins() {
|
|
|
302
306
|
}
|
|
303
307
|
return disabled;
|
|
304
308
|
}
|
|
309
|
+
async function getDisabledPlugins() {
|
|
310
|
+
return parseDisabledPlugins(await runCommand('claude plugin list'));
|
|
311
|
+
}
|
|
305
312
|
export function parseClaudeMdSections(content) {
|
|
306
313
|
const sections = [];
|
|
307
314
|
const lines = content.split('\n');
|
|
@@ -399,8 +406,8 @@ function classifyIssues(localSkills, pluginSkills, brokenSymlinks, memoryFiles,
|
|
|
399
406
|
path: skill.path,
|
|
400
407
|
});
|
|
401
408
|
}
|
|
402
|
-
// Tier 3: oversized skills
|
|
403
|
-
if (skill.sizeBytes >
|
|
409
|
+
// Tier 3: oversized skills
|
|
410
|
+
if (skill.sizeBytes > OVERSIZED_SKILL_BYTES) {
|
|
404
411
|
issues.push({
|
|
405
412
|
type: 'oversized_skill',
|
|
406
413
|
tier: 3,
|
|
@@ -435,9 +442,9 @@ function classifyIssues(localSkills, pluginSkills, brokenSymlinks, memoryFiles,
|
|
|
435
442
|
path: temp.path,
|
|
436
443
|
});
|
|
437
444
|
}
|
|
438
|
-
// Tier 2: oversized memory files
|
|
445
|
+
// Tier 2: oversized memory files
|
|
439
446
|
for (const mem of memoryFiles) {
|
|
440
|
-
if (mem.sizeBytes >
|
|
447
|
+
if (mem.sizeBytes > OVERSIZED_MEMORY_BYTES) {
|
|
441
448
|
issues.push({
|
|
442
449
|
type: 'oversized_memory',
|
|
443
450
|
tier: 2,
|
|
@@ -470,7 +477,7 @@ function classifyIssues(localSkills, pluginSkills, brokenSymlinks, memoryFiles,
|
|
|
470
477
|
tier: 2,
|
|
471
478
|
name: plugin.name,
|
|
472
479
|
detail: `${plugin.skillCount} skills`,
|
|
473
|
-
tokens: plugin.skillCount *
|
|
480
|
+
tokens: plugin.skillCount * SKILL_PROMPT_OVERHEAD_TOKENS,
|
|
474
481
|
path: join(getPluginsDir(), plugin.name),
|
|
475
482
|
});
|
|
476
483
|
}
|
|
@@ -480,6 +487,7 @@ function classifyIssues(localSkills, pluginSkills, brokenSymlinks, memoryFiles,
|
|
|
480
487
|
return issues;
|
|
481
488
|
}
|
|
482
489
|
export async function scan() {
|
|
490
|
+
contentCache.clear();
|
|
483
491
|
const [{ skills: localSkills, brokenSymlinks }, { skills: pluginSkills, plugins, tempCaches }, { memoryFiles, staleProjects }, mcp, disabledPlugins,] = await Promise.all([
|
|
484
492
|
scanLocalSkills(),
|
|
485
493
|
scanPluginSkills(),
|
|
@@ -500,7 +508,7 @@ export async function scan() {
|
|
|
500
508
|
const claudeMdSections = claudeMdContent ? parseClaudeMdSections(claudeMdContent) : [];
|
|
501
509
|
const issues = classifyIssues(localSkills, pluginSkills, brokenSymlinks, memoryFiles, tempCaches, staleProjects, disabledPlugins, plugins);
|
|
502
510
|
// Estimate total tokens at startup
|
|
503
|
-
const skillListingTokens = (localSkills.length + pluginSkills.length) *
|
|
511
|
+
const skillListingTokens = (localSkills.length + pluginSkills.length) * SKILL_PROMPT_OVERHEAD_TOKENS;
|
|
504
512
|
const memoryTokens = memoryFiles.reduce((sum, m) => sum + m.tokens, 0);
|
|
505
513
|
const totalTokensBefore = skillListingTokens + claudeMdTokens + memoryTokens;
|
|
506
514
|
return {
|
package/package.json
CHANGED
|
@@ -41,26 +41,28 @@ bash "${CLAUDE_PLUGIN_ROOT}/skills/claude-slim/scripts/scan.sh"
|
|
|
41
41
|
|
|
42
42
|
After getting the scan JSON, YOU must interpret and present results to the user. Do NOT just dump raw CLI output. Present a full diagnostic report in the user's language.
|
|
43
43
|
|
|
44
|
+
> **Templates below are shown in English for readability. Always translate headers, labels, and prompts into the user's detected language when rendering.**
|
|
45
|
+
|
|
44
46
|
### 2-1. Environment Snapshot Table
|
|
45
47
|
|
|
46
48
|
Show a summary table:
|
|
47
49
|
|
|
48
|
-
|
|
|
49
|
-
|
|
50
|
-
|
|
|
51
|
-
|
|
|
50
|
+
| Item | Count | Tokens |
|
|
51
|
+
|------|-------|--------|
|
|
52
|
+
| Local skills | N (XKB) | X tok |
|
|
53
|
+
| Plugins | N (M skills) | ~X tok |
|
|
52
54
|
| CLAUDE.md | XKB | X tok |
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
+
| Memory files | N (XKB) | ~X tok |
|
|
56
|
+
| **Session startup overhead** | | **~X tok** |
|
|
55
57
|
|
|
56
58
|
### 2-2. Plugin Detail Table
|
|
57
59
|
|
|
58
60
|
List each plugin with skill count and a judgment:
|
|
59
61
|
|
|
60
|
-
|
|
|
61
|
-
|
|
62
|
-
| omc | 36 |
|
|
63
|
-
| temp_local_... | 1 |
|
|
62
|
+
| Plugin | Skills | Notes |
|
|
63
|
+
|--------|:------:|-------|
|
|
64
|
+
| omc | 36 | Core plugin. Keep. |
|
|
65
|
+
| temp_local_... | 1 | **Failed install remnant. Cleanup target.** |
|
|
64
66
|
|
|
65
67
|
Annotate each with status: actively used, possibly unused, or cleanup target. Flag `temp_local_*` entries as failed install remnants.
|
|
66
68
|
|
|
@@ -68,18 +70,18 @@ Annotate each with status: actively used, possibly unused, or cleanup target. Fl
|
|
|
68
70
|
|
|
69
71
|
Group issues by tier and explain EACH one with context and recommendation:
|
|
70
72
|
|
|
71
|
-
**Tier 1 —
|
|
73
|
+
**Tier 1 — Immediate cleanup (zero risk):**
|
|
72
74
|
These are safe to remove with zero risk: broken symlinks, empty templates, .skill/ duplicates, temp_local_* cache. Pre-selected. Explain why each is safe.
|
|
73
75
|
|
|
74
|
-
**Tier 2 —
|
|
76
|
+
**Tier 2 — Recommended cleanup:**
|
|
75
77
|
These are recommended but need user judgment. For each issue, explain:
|
|
76
78
|
- What is it and why it's flagged
|
|
77
79
|
- What happens if you remove it (safe? any side effects?)
|
|
78
80
|
- How many tokens it saves
|
|
79
81
|
|
|
80
|
-
Example: "frontend-design
|
|
82
|
+
Example: "frontend-design exists both locally and in a plugin. Removing the local copy is safe because the plugin version remains. Saves ~823 tok."
|
|
81
83
|
|
|
82
|
-
**Tier 3 —
|
|
84
|
+
**Tier 3 — Optional (user judgment):**
|
|
83
85
|
These are large skills that cost tokens but might be in active use. For each:
|
|
84
86
|
- Show size and token cost
|
|
85
87
|
- Judge whether the user likely uses it (based on what it does)
|
|
@@ -94,7 +96,7 @@ End with a numbered action list, ordered by impact:
|
|
|
94
96
|
|
|
95
97
|
Show estimated total token savings if all recommended actions are taken.
|
|
96
98
|
|
|
97
|
-
If subcommand is `scan`, stop here. Ask "
|
|
99
|
+
If subcommand is `scan`, stop here. Ask a localized equivalent of "Proceed with cleanup?" only for the full pipeline.
|
|
98
100
|
|
|
99
101
|
---
|
|
100
102
|
|
|
@@ -133,7 +135,7 @@ cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js restore
|
|
|
133
135
|
|
|
134
136
|
## Language
|
|
135
137
|
|
|
136
|
-
Detect the user's language from their most recent message. Present all reports, analysis, and explanations in that language. The CLI output is machine-readable
|
|
138
|
+
Detect the user's language from their most recent message. Present all reports, analysis, and explanations in that language — including table headers, tier labels, prompts, and every user-facing string. The CLI output is machine-readable (always English) and must not be echoed verbatim; translate its content into the user's language when you interpret it. The example tables above are written in English only for authoring clarity — do not treat them as a required output format.
|
|
137
139
|
|
|
138
140
|
## Rules
|
|
139
141
|
|