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 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 appendManifest(entry);
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 appendManifest(entry);
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('2.2.0');
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
- const removedSkillCount = movedEntries.filter((e) => e.type !== 'oversized_memory').length;
121
- const totalBefore = result.totalTokensBefore + removedSkillCount * 30;
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
- ...movedEntries
128
- .filter((e) => e.type !== 'oversized_memory')
129
- .map((e) => ({ name: e.name, path: e.from, sizeBytes: 0, tokens: e.tokenCount || 0, source: 'local' })),
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 | null, movedEntries: ManifestEntry[], sessionsPerDay?: number): ReportData;
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 ? scanAfter.totalTokensBefore : before;
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 ? scanAfter.localSkills.length : localBefore - movedEntries.filter((e) => e.type !== 'oversized_memory').length;
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
- lines.push(` ${selected} ${i + 1}. \x1b[${color}m[${tierLabel}]\x1b[0m ${issue.type}: ${issue.name}${detail}${tokStr}`);
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
- async function getDisabledPlugins() {
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 (>10KB)
403
- if (skill.sizeBytes > 10240) {
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 (>5KB)
445
+ // Tier 2: oversized memory files
439
446
  for (const mem of memoryFiles) {
440
- if (mem.sizeBytes > 5120) {
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 * 30,
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) * 30;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-slim",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Analyze and reduce Claude Code token overhead",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- | 로컬 스킬 | N (XKB) | X tok |
51
- | 플러그인 | N (M 스킬) | ~X tok |
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
- | 메모리 파일 | N (XKB) | ~X tok |
54
- | **세션 시작 오버헤드** | | **~X tok** |
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 로컬과 플러그인에 있습니다. 로컬 제거해도 플러그인 버전이 남으니 안전하게 제거 가능. ~823 tok 절감."
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 "정리할까요?" only for the full pipeline.
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 translate only the user-facing interpretation.
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