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 +21 -0
- package/README.md +234 -0
- package/dist/cleaner.d.ts +11 -0
- package/dist/cleaner.js +154 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +218 -0
- package/dist/manifest.d.ts +10 -0
- package/dist/manifest.js +125 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +23 -0
- package/dist/report.d.ts +23 -0
- package/dist/report.js +222 -0
- package/dist/scanner.d.ts +13 -0
- package/dist/scanner.js +520 -0
- package/dist/selection.d.ts +3 -0
- package/dist/selection.js +38 -0
- package/dist/tokenizer.d.ts +5 -0
- package/dist/tokenizer.js +60 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.js +1 -0
- package/package.json +50 -0
- package/skills/claude-slim/SKILL.md +145 -0
- package/skills/claude-slim/scripts/scan.sh +362 -0
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>;
|
package/dist/cleaner.js
ADDED
|
@@ -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
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>;
|