claude-slim 2.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 iops-leo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,234 @@
1
+ <div align="center">
2
+
3
+ # claude-slim
4
+
5
+ **You're burning thousands of tokens before you even say "hello."**
6
+
7
+ Every session loads *every* skill, memory file, and plugin instruction into the system prompt — even the ones you never use. claude-slim finds and removes that waste.
8
+
9
+ ```
10
+ /claude-slim
11
+ ```
12
+
13
+ [한국어](./docs/README.ko.md) | [日本語](./docs/README.ja.md) | [中文](./docs/README.zh.md)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ### The problem, visualized
20
+
21
+ ```
22
+ Session start token budget
23
+ ┌──────────────────────────────────────────────────┐
24
+ │██████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░│ Before claude-slim
25
+ │ 12K tokens consumed ↑ your actual work ↑ │
26
+ ├──────────────────────────────────────────────────┤
27
+ │██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ After claude-slim
28
+ │ 5K ↑ more room for your work ↑ │
29
+ └──────────────────────────────────────────────────┘
30
+ ```
31
+
32
+ Where the bloat hides:
33
+
34
+ | Source | Typical overhead |
35
+ |--------|:---:|
36
+ | 60+ registered skills | ~3,000 tokens |
37
+ | CLAUDE.md (plugin instructions) | ~5,000 tokens |
38
+ | Memory files | ~2,500 tokens |
39
+ | Deferred tools list | ~1,500 tokens |
40
+ | **Total** | **~12,000 tokens** |
41
+
42
+ That's slower responses. Hitting your usage cap faster. Paying for context you're not using.
43
+
44
+ ---
45
+
46
+ ## One command. Five steps.
47
+
48
+ ```
49
+ /claude-slim
50
+ ```
51
+
52
+ ```
53
+ ┌────────┐ ┌──────────┐ ┌─────────┐ ┌─────────┐ ┌────────┐
54
+ │ Scan │ → │ Classify │ → │ Propose │ → │ Clean │ → │ Report │
55
+ │ │ │ │ │ │ │ │ │ │
56
+ │measure │ │ broken │ │ you │ │ move to │ │before │
57
+ │ every │ │ dupes │ │ choose │ │.disabled│ │ vs │
58
+ │source │ │ bloat │ │ what │ │ dir │ │ after │
59
+ └────────┘ └──────────┘ └─────────┘ └─────────┘ └────────┘
60
+ ```
61
+
62
+ **Scan** — Measures everything: local skills, plugin skills, CLAUDE.md, memory files, MCP servers. Token counts are measured with [js-tiktoken](https://github.com/nicolo-ribaudo/js-tiktoken), not guessed.
63
+
64
+ **Classify** — Finds the waste automatically:
65
+
66
+ | | What it catches |
67
+ |---|---|
68
+ | Broken symlinks | Dead links from uninstalled skill packs |
69
+ | Duplicates | Same skill registered from multiple sources |
70
+ | Empty templates | Placeholder skills with no content |
71
+ | Oversized files | SKILL.md over 10KB |
72
+ | Stale memory | Large memory files loaded every session |
73
+ | Disabled plugins | Installed but disabled plugins still in cache |
74
+ | Stale projects | Project memory untouched for 90+ days |
75
+ | Temp caches | Failed plugin install remnants (`temp_local_*`) |
76
+
77
+ **Propose** — Three tiers, you decide:
78
+
79
+ | Tier | Action | Example |
80
+ |------|--------|---------|
81
+ | **Auto** | Pre-selected | Broken symlinks, empty templates, temp caches |
82
+ | **Recommended** | Suggested | Duplicates, stale memory, disabled plugins, stale projects |
83
+ | **Optional** | Your call | Oversized skills you might still use |
84
+
85
+ **Clean** — Moves selected items to `~/.claude/skills.disabled/`. **Nothing is deleted. Ever.**
86
+
87
+ **Report** — Shows exactly what changed:
88
+
89
+ ```
90
+ ╭──────────────────────────────────────────╮
91
+ │ claude-slim report │
92
+ │ │
93
+ │ Before: 14,510 tokens at startup │
94
+ │ After: 5,181 tokens at startup │
95
+ │ Saved: 9,329 tokens (64.3%) │
96
+ │ │
97
+ │ Top offenders removed: │
98
+ │ • office-hours 23,008 tok │
99
+ │ • harness 7,902 tok │
100
+ │ • manpower 4,764 tok │
101
+ │ │
102
+ │ Est. monthly savings: ~$1.68 │
103
+ │ (2 sessions/day × $0.003/1K tok) │
104
+ ╰──────────────────────────────────────────╯
105
+
106
+ ┌──────────────────┬──────────┬──────────┬────────────┐
107
+ │ │ Before │ After │ Saved │
108
+ ├──────────────────┼──────────┼──────────┼────────────┤
109
+ │ Local skills │ 14 │ 4 │ -10 │
110
+ │ System prompt │ ~124 │ ~114 │ -10 │
111
+ │ Memory files │ 19.5KB │ 5.7KB │ -13.8KB │
112
+ │ Est. tokens │ ~14,510 │ ~5,181 │ ~9,329 │
113
+ └──────────────────┴──────────┴──────────┴────────────┘
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Install (10 seconds)
119
+
120
+ ```bash
121
+ claude plugin marketplace add iops-leo/claude-slim
122
+ claude plugin install claude-slim
123
+ ```
124
+
125
+ Then just type `/claude-slim` in any session.
126
+
127
+ Or use the standalone CLI:
128
+
129
+ ```bash
130
+ npx claude-slim scan
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Usage
136
+
137
+ ```bash
138
+ /claude-slim # Full pipeline: scan → propose → clean → report
139
+ /claude-slim scan # Report only, no changes
140
+ /claude-slim scan --json # Machine-readable JSON output
141
+ /claude-slim restore # Bring back anything you disabled
142
+ ```
143
+
144
+ CLI equivalents:
145
+
146
+ ```bash
147
+ npx claude-slim clean # Full pipeline
148
+ npx claude-slim clean --dry-run # See what would happen (no changes)
149
+ npx claude-slim clean --auto # Non-interactive, Tier 1 only (CI/scripts)
150
+ npx claude-slim scan # Report only
151
+ npx claude-slim restore # Undo
152
+ npx claude-slim report # Show savings from last clean
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Safety first
158
+
159
+ | | |
160
+ |---|---|
161
+ | **Non-destructive** | Nothing is ever deleted. Disabled items move to `~/.claude/skills.disabled/` |
162
+ | **Reversible** | `/claude-slim restore` brings anything back, any time |
163
+ | **User-controlled** | Always asks before making changes. `--dry-run` to preview. |
164
+ | **Hands off** | Never touches CLAUDE.md, settings.json, or plugin configs |
165
+
166
+ ---
167
+
168
+ ## How it works
169
+
170
+ claude-slim scans these locations. No plugin-specific logic — pure filesystem analysis.
171
+
172
+ ```
173
+ ~/.claude/
174
+ ├── skills/ ← user-installed skills
175
+ ├── plugins/cache/ ← plugin skills
176
+ ├── CLAUDE.md ← system instructions (read-only)
177
+ ├── projects/*/memory/ ← auto-memory files
178
+ └── settings.json ← MCP server count (read-only)
179
+ ```
180
+
181
+ Works with any setup: OMC, gstack, marketplace plugins, custom skills, or vanilla Claude Code.
182
+
183
+ ---
184
+
185
+ ## Real-world results
186
+
187
+ From a real cleanup session:
188
+
189
+ | Metric | Before | After | |
190
+ |--------|:------:|:-----:|---|
191
+ | Local skills | 65 | 15 | **-77%** |
192
+ | System prompt skills | ~80 | ~48 | **-40%** |
193
+ | Memory files | 15KB | 2KB | **-87%** |
194
+ | **Est. token savings** | | **~4,300/session** | |
195
+
196
+ ---
197
+
198
+ ## v2.2 — What's new
199
+
200
+ - **Atomic `stale_project` clean/restore** — Single directory `rename()` instead of per-file loop. No more partial-failure state if the operation is interrupted.
201
+ - **Clear collision errors** — Cleaning a project whose backup already exists (or restoring onto an existing directory) now fails with an actionable message instead of a cryptic OS error.
202
+ - **Manifest schema v2** — Single JSON file (`manifest.json`) containing only currently-disabled entries. Restore removes the entry entirely, so the manifest stays bounded across many clean/restore cycles.
203
+ - **Automatic migration from v1** — Existing legacy manifests (`.claude-slim-manifest.jsonl`) are auto-migrated on first read; the original is preserved as `.jsonl.bak` for safety.
204
+ - **Crash-safe manifest writes** — Write-to-tmp-then-rename pattern prevents partial-write corruption on power loss or SIGKILL.
205
+ - **Expanded test coverage** — 66 tests (was 35). New round-trip tests per issue type: `broken_symlink`, `template`, `duplicate`, `skill_dup`, `oversized_skill`, `temp_cache`, `stale_project`. Plus manifest migration + bounded-growth cycle tests.
206
+
207
+ ## v2.0 — What's new
208
+
209
+ - **TypeScript CLI** — Rewritten from bash. Faster, more accurate, extensible.
210
+ - **Accurate token counting** — [js-tiktoken](https://github.com/nicolo-ribaudo/js-tiktoken) with `cl100k_base` encoding.
211
+ - **Savings report box** — Visual before/after with breakdown table and monthly savings estimate.
212
+ - **`--dry-run`** — Preview changes without making them.
213
+ - **`--json`** — Machine-readable output for automation.
214
+ - **Token cache** — Instant repeat scans.
215
+ - **Standalone CLI** — `npx claude-slim` works outside Claude Code.
216
+ - **`--auto`** — Non-interactive cleanup for CI/scripts (Tier 1 only).
217
+ - **Disabled plugin detection** — Finds plugins you disabled but didn't uninstall.
218
+ - **Stale project detection** — Flags project memory untouched for 90+ days.
219
+ - **CLAUDE.md section breakdown** — See which plugin instructions cost the most tokens.
220
+ - **Plugin status** — Shows enabled/disabled status for each plugin.
221
+ - **Non-TTY support** — Auto-selects Tier 1 when stdin is piped.
222
+ - **Unit tests** — Vitest-based.
223
+
224
+ ---
225
+
226
+ ## Requirements
227
+
228
+ - Node.js 18+
229
+ - macOS or Linux
230
+ - Claude Code CLI
231
+
232
+ ## License
233
+
234
+ MIT
@@ -0,0 +1,11 @@
1
+ import type { Issue, ManifestEntry } from './types.js';
2
+ export interface CleanResult {
3
+ moved: ManifestEntry[];
4
+ skipped: string[];
5
+ errors: Array<{
6
+ name: string;
7
+ error: string;
8
+ }>;
9
+ }
10
+ export declare function cleanIssues(issues: Issue[]): Promise<CleanResult>;
11
+ export declare function restoreItem(entry: ManifestEntry): Promise<void>;
@@ -0,0 +1,154 @@
1
+ import { rename, readdir, rmdir, rm, unlink, lstat, mkdir } from 'node:fs/promises';
2
+ import { join, dirname } from 'node:path';
3
+ import { appendManifest, ensureDisabledDir, getDisabledDir, removeEntry } from './manifest.js';
4
+ import { getSkillsDir } from './paths.js';
5
+ async function pathExists(p) {
6
+ try {
7
+ await lstat(p);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ export async function cleanIssues(issues) {
15
+ await ensureDisabledDir();
16
+ const disabledDir = getDisabledDir();
17
+ const moved = [];
18
+ const skipped = [];
19
+ const errors = [];
20
+ for (const issue of issues) {
21
+ try {
22
+ if (issue.type === 'broken_symlink') {
23
+ await unlink(issue.path);
24
+ const entry = {
25
+ date: new Date().toISOString(),
26
+ name: issue.name,
27
+ from: issue.path,
28
+ type: issue.type,
29
+ tokenCount: issue.tokens,
30
+ tier: issue.tier,
31
+ };
32
+ await appendManifest(entry);
33
+ moved.push(entry);
34
+ }
35
+ else if (issue.type === 'template' ||
36
+ issue.type === 'duplicate' ||
37
+ issue.type === 'skill_dup' ||
38
+ issue.type === 'oversized_skill') {
39
+ // Move skill directory to disabled — use name (not basename) to avoid namespace collisions
40
+ const safeName = issue.name.replace(/\//g, '--');
41
+ const dest = join(disabledDir, safeName);
42
+ await rename(issue.path, dest);
43
+ const entry = {
44
+ date: new Date().toISOString(),
45
+ name: issue.name,
46
+ from: issue.path,
47
+ type: issue.type,
48
+ tokenCount: issue.tokens,
49
+ tier: issue.tier,
50
+ };
51
+ await appendManifest(entry);
52
+ moved.push(entry);
53
+ }
54
+ else if (issue.type === 'temp_cache') {
55
+ // Delete temp directories (failed plugin installs, not restorable)
56
+ await rm(issue.path, { recursive: true, force: true });
57
+ const entry = {
58
+ date: new Date().toISOString(),
59
+ name: issue.name,
60
+ from: issue.path,
61
+ type: issue.type,
62
+ tokenCount: 0,
63
+ tier: issue.tier,
64
+ };
65
+ await appendManifest(entry);
66
+ moved.push(entry);
67
+ }
68
+ else if (issue.type === 'stale_project') {
69
+ const backupParent = join(disabledDir, 'memory-backup');
70
+ await mkdir(backupParent, { recursive: true });
71
+ const backupDir = join(backupParent, issue.name);
72
+ // Refuse to overwrite an existing backup — protects prior clean state
73
+ if (await pathExists(backupDir)) {
74
+ throw new Error(`Backup already exists for "${issue.name}" at ${backupDir}. ` +
75
+ `Restore or remove it before cleaning again.`);
76
+ }
77
+ // Atomic directory rename — requires same FS (guaranteed since both paths are under ~/.claude/)
78
+ await rename(issue.path, backupDir);
79
+ const entry = {
80
+ date: new Date().toISOString(),
81
+ name: issue.name,
82
+ from: issue.path,
83
+ type: issue.type,
84
+ tokenCount: issue.tokens,
85
+ tier: issue.tier,
86
+ };
87
+ await appendManifest(entry);
88
+ moved.push(entry);
89
+ }
90
+ else if (issue.type === 'oversized_memory' || issue.type === 'disabled_plugin') {
91
+ // Report only — user manages these manually
92
+ skipped.push(issue.name);
93
+ }
94
+ }
95
+ catch (err) {
96
+ const message = err instanceof Error ? err.message : String(err);
97
+ errors.push({ name: issue.name, error: message });
98
+ }
99
+ }
100
+ // Clean empty directories in skills/
101
+ await cleanEmptyDirs(getSkillsDir());
102
+ return { moved, skipped, errors };
103
+ }
104
+ async function cleanEmptyDirs(dir) {
105
+ try {
106
+ const entries = await readdir(dir);
107
+ for (const entry of entries) {
108
+ const entryPath = join(dir, entry);
109
+ try {
110
+ const stats = await lstat(entryPath);
111
+ if (stats.isDirectory()) {
112
+ await cleanEmptyDirs(entryPath);
113
+ const remaining = await readdir(entryPath);
114
+ if (remaining.length === 0) {
115
+ await rmdir(entryPath);
116
+ }
117
+ }
118
+ }
119
+ catch { /* skip */ }
120
+ }
121
+ }
122
+ catch { /* skip */ }
123
+ }
124
+ export async function restoreItem(entry) {
125
+ if (entry.type === 'broken_symlink') {
126
+ throw new Error(`Broken symlinks cannot be restored (${entry.name})`);
127
+ }
128
+ if (entry.type === 'disabled_plugin') {
129
+ throw new Error(`Plugins must be reinstalled: claude plugin install ${entry.name}`);
130
+ }
131
+ if (entry.type === 'temp_cache') {
132
+ throw new Error(`Temp caches were deleted and cannot be restored (${entry.name})`);
133
+ }
134
+ const disabledDir = getDisabledDir();
135
+ if (entry.type === 'stale_project') {
136
+ const backupDir = join(disabledDir, 'memory-backup', entry.name);
137
+ // Refuse to overwrite user's current state
138
+ if (await pathExists(entry.from)) {
139
+ throw new Error(`Cannot restore: ${entry.from} already exists. ` +
140
+ `Remove or rename it first.`);
141
+ }
142
+ await mkdir(dirname(entry.from), { recursive: true });
143
+ // Atomic directory rename — requires same FS (guaranteed since both paths are under ~/.claude/)
144
+ await rename(backupDir, entry.from);
145
+ }
146
+ else {
147
+ // Restore skill directory using the same naming as cleanIssues
148
+ const safeName = entry.name.replace(/\//g, '--');
149
+ const src = join(disabledDir, safeName);
150
+ await mkdir(dirname(entry.from), { recursive: true });
151
+ await rename(src, entry.from);
152
+ }
153
+ await removeEntry(entry.name);
154
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from 'node:readline';
3
+ import { Command } from 'commander';
4
+ import { initTokenizer, flushCache } from './tokenizer.js';
5
+ import { scan } from './scanner.js';
6
+ import { cleanIssues, restoreItem } from './cleaner.js';
7
+ import { readManifest } from './manifest.js';
8
+ import { formatScanSummary, formatReportBox, calculateReport, } from './report.js';
9
+ import { resolveSelection, resolveRestoreSelection } from './selection.js';
10
+ const program = new Command();
11
+ program
12
+ .name('claude-slim')
13
+ .description('Analyze and reduce Claude Code token overhead')
14
+ .version('2.2.0');
15
+ // --- scan ---
16
+ program
17
+ .command('scan')
18
+ .description('Scan environment and report issues')
19
+ .option('--json', 'Output raw JSON')
20
+ .action(async (opts) => {
21
+ await initTokenizer();
22
+ const result = await scan();
23
+ await flushCache();
24
+ if (opts.json) {
25
+ console.log(JSON.stringify(result, null, 2));
26
+ }
27
+ else {
28
+ console.log(formatScanSummary(result));
29
+ }
30
+ });
31
+ // --- clean ---
32
+ program
33
+ .command('clean')
34
+ .description('Interactive cleanup of detected issues')
35
+ .option('--dry-run', 'Show what would happen without making changes')
36
+ .option('--auto', 'Non-interactive: auto-select Tier 1 items only')
37
+ .option('--sessions-per-day <n>', 'Sessions per day for savings estimate', '2')
38
+ .action(async (opts) => {
39
+ await runCleanPipeline({
40
+ dryRun: !!opts.dryRun,
41
+ auto: !!opts.auto,
42
+ sessionsPerDay: parseInt(opts.sessionsPerDay, 10) || 2,
43
+ });
44
+ });
45
+ // --- restore ---
46
+ program
47
+ .command('restore')
48
+ .description('Restore previously disabled items')
49
+ .action(async () => {
50
+ const entries = await readManifest();
51
+ // v2 manifest contains only currently-disabled entries (restored ones are removed)
52
+ const disabled = entries.filter((e) => e.type !== 'broken_symlink');
53
+ const NON_RESTORABLE = new Set(['temp_cache', 'disabled_plugin']);
54
+ const restorable = disabled.filter((e) => !NON_RESTORABLE.has(e.type));
55
+ const infoOnly = disabled.filter((e) => NON_RESTORABLE.has(e.type));
56
+ if (restorable.length === 0 && infoOnly.length === 0) {
57
+ console.log('\n Nothing to restore.\n');
58
+ return;
59
+ }
60
+ if (restorable.length > 0) {
61
+ console.log('\n Restorable items:\n');
62
+ for (let i = 0; i < restorable.length; i++) {
63
+ const e = restorable[i];
64
+ const date = new Date(e.date).toLocaleDateString();
65
+ console.log(` ${i + 1}. ${e.name} (${e.type}, disabled ${date})`);
66
+ }
67
+ }
68
+ if (infoOnly.length > 0) {
69
+ console.log('\n \x1b[90mNon-restorable (for reference):\x1b[0m');
70
+ for (const e of infoOnly) {
71
+ const date = new Date(e.date).toLocaleDateString();
72
+ const hint = e.type === 'disabled_plugin'
73
+ ? `reinstall: claude plugin install ${e.name}`
74
+ : 'deleted, cannot restore';
75
+ console.log(` \x1b[90m\u2022 ${e.name} (${date}) — ${hint}\x1b[0m`);
76
+ }
77
+ }
78
+ if (restorable.length === 0) {
79
+ console.log('\n No items can be restored.\n');
80
+ return;
81
+ }
82
+ console.log('');
83
+ const selection = await askUser(' Restore (all / numbers / none): ');
84
+ const indices = resolveRestoreSelection(selection, restorable.length);
85
+ if (indices.length === 0) {
86
+ console.log('\n Cancelled.\n');
87
+ return;
88
+ }
89
+ let restored = 0;
90
+ for (const idx of indices) {
91
+ try {
92
+ await restoreItem(restorable[idx]);
93
+ console.log(` \x1b[32m\u2713\x1b[0m Restored: ${restorable[idx].name}`);
94
+ restored++;
95
+ }
96
+ catch (err) {
97
+ const msg = err instanceof Error ? err.message : String(err);
98
+ console.log(` \x1b[31m\u2717\x1b[0m ${restorable[idx].name}: ${msg}`);
99
+ }
100
+ }
101
+ console.log(`\n Restored ${restored} item(s).\n`);
102
+ });
103
+ // --- report ---
104
+ program
105
+ .command('report')
106
+ .description('Show savings report from last clean')
107
+ .option('--sessions-per-day <n>', 'Sessions per day for savings estimate', '2')
108
+ .action(async (opts) => {
109
+ await initTokenizer();
110
+ const result = await scan();
111
+ const entries = await readManifest();
112
+ const movedEntries = entries.filter((e) => e.tokenCount && e.tokenCount > 0);
113
+ if (movedEntries.length === 0) {
114
+ console.log('\n No previous cleanup found. Run `claude-slim clean` first.\n');
115
+ await flushCache();
116
+ return;
117
+ }
118
+ 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;
122
+ const pseudoBefore = {
123
+ ...result,
124
+ totalTokensBefore: totalBefore,
125
+ localSkills: [
126
+ ...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' })),
130
+ ],
131
+ };
132
+ const reportData = calculateReport(pseudoBefore, result, movedEntries, sessionsPerDay);
133
+ console.log('');
134
+ console.log(formatReportBox(reportData));
135
+ console.log('');
136
+ await flushCache();
137
+ });
138
+ // --- default (no subcommand) → run clean ---
139
+ program.action(async () => {
140
+ await runCleanPipeline({ dryRun: false, auto: false, sessionsPerDay: 2 });
141
+ });
142
+ // --- shared clean pipeline ---
143
+ async function runCleanPipeline(opts) {
144
+ await initTokenizer();
145
+ const result = await scan();
146
+ if (result.issues.length === 0) {
147
+ console.log('\n \x1b[32mAlready slim!\x1b[0m No issues found.\n');
148
+ await flushCache();
149
+ return;
150
+ }
151
+ console.log(formatScanSummary(result));
152
+ const isInteractive = !opts.auto && process.stdin.isTTY;
153
+ let selectedIssues;
154
+ if (isInteractive) {
155
+ console.log(' Actions:');
156
+ console.log(' Enter \u2192 accept pre-selected (Tier 1 only)');
157
+ console.log(' 1,3,5 \u2192 select specific items');
158
+ console.log(' all \u2192 select everything');
159
+ console.log(' none \u2192 cancel');
160
+ console.log('');
161
+ const selection = await askUser(' Your choice: ');
162
+ selectedIssues = resolveSelection(selection, result.issues);
163
+ }
164
+ else {
165
+ // Auto mode or non-TTY: select Tier 1 only
166
+ selectedIssues = result.issues.filter((i) => i.tier === 1);
167
+ if (!opts.auto) {
168
+ console.log(' \x1b[33m\u26a0 Non-interactive mode detected, auto-selecting Tier 1\x1b[0m\n');
169
+ }
170
+ else {
171
+ console.log(` \x1b[36m\u2192 Auto mode: selecting ${selectedIssues.length} Tier 1 item(s)\x1b[0m\n`);
172
+ }
173
+ }
174
+ if (selectedIssues.length === 0) {
175
+ console.log('\n Cancelled. No changes made.\n');
176
+ await flushCache();
177
+ return;
178
+ }
179
+ if (opts.dryRun) {
180
+ console.log('\n \x1b[33m[DRY RUN]\x1b[0m Would disable:');
181
+ for (const issue of selectedIssues) {
182
+ console.log(` \u2022 ${issue.name} (${issue.type})`);
183
+ }
184
+ console.log(`\n Estimated token savings: ~${selectedIssues.reduce((s, i) => s + i.tokens, 0).toLocaleString()}`);
185
+ console.log('');
186
+ await flushCache();
187
+ return;
188
+ }
189
+ console.log('');
190
+ const cleanResult = await cleanIssues(selectedIssues);
191
+ // Re-scan after cleanup for accurate breakdown
192
+ const afterResult = await scan();
193
+ const reportData = calculateReport(result, afterResult, cleanResult.moved, opts.sessionsPerDay);
194
+ console.log('');
195
+ console.log(formatReportBox(reportData));
196
+ console.log('');
197
+ if (cleanResult.errors.length > 0) {
198
+ console.log(' \x1b[31mErrors:\x1b[0m');
199
+ for (const err of cleanResult.errors) {
200
+ console.log(` \u2022 ${err.name}: ${err.error}`);
201
+ }
202
+ console.log('');
203
+ }
204
+ console.log(` Recovery: ~/.claude/skills.disabled/`);
205
+ console.log(` Restore: npx claude-slim restore\n`);
206
+ await flushCache();
207
+ }
208
+ // --- helpers ---
209
+ function askUser(prompt) {
210
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
211
+ return new Promise((resolve) => {
212
+ rl.question(prompt, (answer) => {
213
+ rl.close();
214
+ resolve(answer.trim());
215
+ });
216
+ });
217
+ }
218
+ program.parse();
@@ -0,0 +1,10 @@
1
+ import type { Manifest, ManifestEntry } from './types.js';
2
+ export declare function getDisabledDir(): string;
3
+ export declare function ensureDisabledDir(): Promise<void>;
4
+ export declare function migrateLegacyIfNeeded(): Promise<void>;
5
+ export declare function readManifestV2(): Promise<Manifest>;
6
+ export declare function writeManifestV2(manifest: Manifest): Promise<void>;
7
+ export declare function addEntry(entry: ManifestEntry): Promise<void>;
8
+ export declare function removeEntry(name: string): Promise<ManifestEntry | null>;
9
+ export declare function readManifest(): Promise<ManifestEntry[]>;
10
+ export declare function appendManifest(entry: ManifestEntry): Promise<void>;