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/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-slim",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Analyze and reduce Claude Code token overhead",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-slim": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"skills",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsc --watch",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"claude",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"token",
|
|
26
|
+
"optimization",
|
|
27
|
+
"cleanup",
|
|
28
|
+
"skills",
|
|
29
|
+
"plugin"
|
|
30
|
+
],
|
|
31
|
+
"author": "iops-leo",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/iops-leo/claude-slim.git"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"commander": "^13.0.0",
|
|
42
|
+
"js-tiktoken": "^1.0.18"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
|
+
"tmp-promise": "^3.0.3",
|
|
47
|
+
"typescript": "^5.7.0",
|
|
48
|
+
"vitest": "^4.1.4"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claude-slim
|
|
3
|
+
description: "Analyze and reduce Claude Code token overhead. Scans skills, plugins, memory files, and CLAUDE.md for bloat — then cleans up with user approval. Use when: /claude-slim, token optimization, reduce tokens, slim down, cleanup skills, token diet, save tokens, context diet"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# claude-slim — Token Overhead Reducer
|
|
7
|
+
|
|
8
|
+
Analyze the user's Claude Code environment for token waste and perform non-destructive cleanup.
|
|
9
|
+
|
|
10
|
+
## Subcommands
|
|
11
|
+
|
|
12
|
+
- `/claude-slim` or `/claude-slim run` → full pipeline (scan → propose → execute → report)
|
|
13
|
+
- `/claude-slim scan` → report only, no changes
|
|
14
|
+
- `/claude-slim scan --json` → raw JSON output
|
|
15
|
+
- `/claude-slim restore` → restore previously disabled items
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Phase 1 — Scan
|
|
20
|
+
|
|
21
|
+
Run the CLI to collect environment data:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js scan --json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If `CLAUDE_PLUGIN_ROOT` is not set:
|
|
28
|
+
```bash
|
|
29
|
+
PLUGIN_DIR=$(find ~/.claude/plugins -path "*/claude-slim/dist/cli.js" -type f 2>/dev/null | head -1 | xargs dirname | xargs dirname)
|
|
30
|
+
cd "$PLUGIN_DIR" && node dist/cli.js scan --json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
If `node` is not available, fall back to the legacy bash scanner:
|
|
34
|
+
```bash
|
|
35
|
+
bash "${CLAUDE_PLUGIN_ROOT}/skills/claude-slim/scripts/scan.sh"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Phase 2 — Interpret & Present
|
|
41
|
+
|
|
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
|
+
|
|
44
|
+
### 2-1. Environment Snapshot Table
|
|
45
|
+
|
|
46
|
+
Show a summary table:
|
|
47
|
+
|
|
48
|
+
| 항목 | 수치 | 토큰 |
|
|
49
|
+
|------|------|------|
|
|
50
|
+
| 로컬 스킬 | N개 (XKB) | X tok |
|
|
51
|
+
| 플러그인 | N개 (M 스킬) | ~X tok |
|
|
52
|
+
| CLAUDE.md | XKB | X tok |
|
|
53
|
+
| 메모리 파일 | N개 (XKB) | ~X tok |
|
|
54
|
+
| **세션 시작 오버헤드** | | **~X tok** |
|
|
55
|
+
|
|
56
|
+
### 2-2. Plugin Detail Table
|
|
57
|
+
|
|
58
|
+
List each plugin with skill count and a judgment:
|
|
59
|
+
|
|
60
|
+
| 플러그인 | 스킬 수 | 비고 |
|
|
61
|
+
|----------|:-------:|------|
|
|
62
|
+
| omc | 36 | 코어 플러그인. 유지 |
|
|
63
|
+
| temp_local_... | 1 | **실패한 설치 잔여물. 삭제 대상** |
|
|
64
|
+
|
|
65
|
+
Annotate each with status: actively used, possibly unused, or cleanup target. Flag `temp_local_*` entries as failed install remnants.
|
|
66
|
+
|
|
67
|
+
### 2-3. Issue Analysis by Tier
|
|
68
|
+
|
|
69
|
+
Group issues by tier and explain EACH one with context and recommendation:
|
|
70
|
+
|
|
71
|
+
**Tier 1 — 즉시 정리 (위험 없음):**
|
|
72
|
+
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
|
+
|
|
74
|
+
**Tier 2 — 정리 추천:**
|
|
75
|
+
These are recommended but need user judgment. For each issue, explain:
|
|
76
|
+
- What is it and why it's flagged
|
|
77
|
+
- What happens if you remove it (safe? any side effects?)
|
|
78
|
+
- How many tokens it saves
|
|
79
|
+
|
|
80
|
+
Example: "frontend-design이 로컬과 플러그인에 둘 다 있습니다. 로컬 제거해도 플러그인 버전이 남으니 안전하게 제거 가능. ~823 tok 절감."
|
|
81
|
+
|
|
82
|
+
**Tier 3 — 선택 사항 (사용자 판단):**
|
|
83
|
+
These are large skills that cost tokens but might be in active use. For each:
|
|
84
|
+
- Show size and token cost
|
|
85
|
+
- Judge whether the user likely uses it (based on what it does)
|
|
86
|
+
- Recommend: keep if active, disable if rarely used
|
|
87
|
+
|
|
88
|
+
### 2-4. Recommended Actions
|
|
89
|
+
|
|
90
|
+
End with a numbered action list, ordered by impact:
|
|
91
|
+
1. What to do first (highest token savings, lowest risk)
|
|
92
|
+
2. What to consider
|
|
93
|
+
3. What to leave alone and why
|
|
94
|
+
|
|
95
|
+
Show estimated total token savings if all recommended actions are taken.
|
|
96
|
+
|
|
97
|
+
If subcommand is `scan`, stop here. Ask "정리할까요?" only for the full pipeline.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Phase 3 — Clean (full pipeline only)
|
|
102
|
+
|
|
103
|
+
Run the interactive clean command:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js clean
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Or with dry-run:
|
|
110
|
+
```bash
|
|
111
|
+
cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js clean --dry-run
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
After cleanup, re-run scan to get updated numbers, then show the savings report:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js report
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Present the report box AND the before/after breakdown table to the user.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Phase 4 — Restore
|
|
125
|
+
|
|
126
|
+
When `/claude-slim restore` is invoked:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
cd "${CLAUDE_PLUGIN_ROOT}" && node dist/cli.js restore
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Language
|
|
135
|
+
|
|
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.
|
|
137
|
+
|
|
138
|
+
## Rules
|
|
139
|
+
|
|
140
|
+
1. **Never delete.** The CLI moves items to `~/.claude/skills.disabled/`.
|
|
141
|
+
2. **Never modify CLAUDE.md or settings.json.**
|
|
142
|
+
3. **Never disable plugin-managed skills.** Report only.
|
|
143
|
+
4. **Always confirm before executing.** Use `--dry-run` to preview changes.
|
|
144
|
+
5. **If nothing to clean:** respond "Already slim!" and exit.
|
|
145
|
+
6. **Always interpret results.** Never dump raw CLI output without analysis. You are the diagnostic layer — the CLI is the data layer.
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# claude-slim scanner — detects token overhead in Claude Code environments
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
OUTPUT_FORMAT="${1:-text}" # text (default) or json
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
|
|
8
|
+
# JSON mode: re-run in text mode, pipe through converter
|
|
9
|
+
if [ "$OUTPUT_FORMAT" = "json" ]; then
|
|
10
|
+
bash "${SCRIPT_DIR}/scan.sh" text | python3 -c "
|
|
11
|
+
import json, sys
|
|
12
|
+
|
|
13
|
+
data = {}
|
|
14
|
+
for line in sys.stdin:
|
|
15
|
+
line = line.strip()
|
|
16
|
+
if not line or line.startswith('==='):
|
|
17
|
+
continue
|
|
18
|
+
parts = line.split(':')
|
|
19
|
+
key = parts[0]
|
|
20
|
+
if key == 'SKILL':
|
|
21
|
+
# name may contain ':', size is always the last field
|
|
22
|
+
data.setdefault('skills', []).append({'name': ':'.join(parts[1:-1]), 'size': int(parts[-1])})
|
|
23
|
+
elif key == 'BROKEN_SYMLINK':
|
|
24
|
+
data.setdefault('broken_symlinks', []).append({'name': parts[1], 'target': ':'.join(parts[2:])})
|
|
25
|
+
elif key == 'LOCAL_SUMMARY':
|
|
26
|
+
data['local_count'] = int(parts[1])
|
|
27
|
+
data['local_total_bytes'] = int(parts[2])
|
|
28
|
+
elif key == 'PLUGIN':
|
|
29
|
+
p = {'name': parts[1], 'skill_count': int(parts[2])}
|
|
30
|
+
if len(parts) > 3: p['status'] = parts[3]
|
|
31
|
+
data.setdefault('plugins', []).append(p)
|
|
32
|
+
elif key == 'PLUGIN_SKILL':
|
|
33
|
+
data.setdefault('plugin_skills', []).append({'plugin': parts[1], 'name': ':'.join(parts[2:])})
|
|
34
|
+
elif key == 'CLAUDE_MD':
|
|
35
|
+
data['claude_md_bytes'] = int(parts[1])
|
|
36
|
+
elif key == 'CLAUDE_MD_SECTION':
|
|
37
|
+
# name may contain ':', size is always last
|
|
38
|
+
data.setdefault('claude_md_sections', []).append({'name': ':'.join(parts[1:-1]), 'size': int(parts[-1])})
|
|
39
|
+
elif key == 'MEMORY':
|
|
40
|
+
# MEMORY:project:name:size — size is always last
|
|
41
|
+
data.setdefault('memory_files', []).append({
|
|
42
|
+
'project': ':'.join(parts[1:-2]),
|
|
43
|
+
'name': parts[-2],
|
|
44
|
+
'size': int(parts[-1])
|
|
45
|
+
})
|
|
46
|
+
elif key == 'MEMORY_SUMMARY':
|
|
47
|
+
data['memory_count'] = int(parts[1])
|
|
48
|
+
data['memory_total_bytes'] = int(parts[2])
|
|
49
|
+
elif key == 'MCP':
|
|
50
|
+
data['mcp_servers'] = int(parts[1])
|
|
51
|
+
elif key == 'MCP_SERVER':
|
|
52
|
+
data.setdefault('mcp_server_list', []).append(':'.join(parts[1:]))
|
|
53
|
+
elif key == 'ISSUE':
|
|
54
|
+
issue = {'type': parts[1]}
|
|
55
|
+
if parts[1] in ('oversized_skill', 'oversized_memory', 'temp_cache', 'stale_project'):
|
|
56
|
+
# detail is last field, name is everything between type and detail
|
|
57
|
+
issue['name'] = ':'.join(parts[2:-1])
|
|
58
|
+
issue['detail'] = parts[-1]
|
|
59
|
+
elif len(parts) > 3:
|
|
60
|
+
issue['name'] = parts[2]
|
|
61
|
+
issue['detail'] = ':'.join(parts[3:])
|
|
62
|
+
else:
|
|
63
|
+
issue['name'] = ':'.join(parts[2:])
|
|
64
|
+
data.setdefault('issues', []).append(issue)
|
|
65
|
+
|
|
66
|
+
print(json.dumps(data, indent=2))
|
|
67
|
+
"
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
CLAUDE_DIR="${HOME}/.claude"
|
|
72
|
+
SKILLS_DIR="${CLAUDE_DIR}/skills"
|
|
73
|
+
PLUGINS_DIR="${CLAUDE_DIR}/plugins/cache"
|
|
74
|
+
PROJECTS_DIR="${CLAUDE_DIR}/projects"
|
|
75
|
+
|
|
76
|
+
# Cache `claude plugin list` output once (reused for status + issue detection)
|
|
77
|
+
PLUGIN_LIST_CACHE=""
|
|
78
|
+
if command -v claude >/dev/null 2>&1; then
|
|
79
|
+
PLUGIN_LIST_CACHE=$(claude plugin list 2>/dev/null || true)
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
echo "=== LOCAL SKILLS ==="
|
|
83
|
+
local_count=0
|
|
84
|
+
local_total_bytes=0
|
|
85
|
+
if [ -d "$SKILLS_DIR" ] && [ "$(ls -A "$SKILLS_DIR" 2>/dev/null)" ]; then
|
|
86
|
+
# Top-level skills
|
|
87
|
+
for dir in "$SKILLS_DIR"/*/; do
|
|
88
|
+
[ -d "$dir" ] || continue
|
|
89
|
+
name=$(basename "$dir")
|
|
90
|
+
if [ -f "${dir}SKILL.md" ]; then
|
|
91
|
+
size=$(wc -c < "${dir}SKILL.md" | tr -d ' ')
|
|
92
|
+
echo "SKILL:${name}:${size}"
|
|
93
|
+
local_total_bytes=$((local_total_bytes + size))
|
|
94
|
+
local_count=$((local_count + 1))
|
|
95
|
+
elif [ -L "${dir}SKILL.md" ] && [ ! -e "${dir}SKILL.md" ]; then
|
|
96
|
+
echo "BROKEN_SYMLINK:${name}:$(readlink "${dir}SKILL.md" 2>/dev/null || echo unknown)"
|
|
97
|
+
fi
|
|
98
|
+
done
|
|
99
|
+
# Nested skills (e.g., @internal-sys/commit-guide/)
|
|
100
|
+
for dir in "$SKILLS_DIR"/*/*/; do
|
|
101
|
+
[ -d "$dir" ] || continue
|
|
102
|
+
parent=$(basename "$(dirname "$dir")")
|
|
103
|
+
name="${parent}/$(basename "$dir")"
|
|
104
|
+
if [ -f "${dir}SKILL.md" ]; then
|
|
105
|
+
size=$(wc -c < "${dir}SKILL.md" | tr -d ' ')
|
|
106
|
+
echo "SKILL:${name}:${size}"
|
|
107
|
+
local_total_bytes=$((local_total_bytes + size))
|
|
108
|
+
local_count=$((local_count + 1))
|
|
109
|
+
elif [ -L "${dir}SKILL.md" ] && [ ! -e "${dir}SKILL.md" ]; then
|
|
110
|
+
echo "BROKEN_SYMLINK:${name}:$(readlink "${dir}SKILL.md" 2>/dev/null || echo unknown)"
|
|
111
|
+
fi
|
|
112
|
+
done
|
|
113
|
+
fi
|
|
114
|
+
echo "LOCAL_SUMMARY:${local_count}:${local_total_bytes}"
|
|
115
|
+
|
|
116
|
+
echo ""
|
|
117
|
+
echo "=== PLUGIN SKILLS ==="
|
|
118
|
+
# Parse disabled plugins from cached output
|
|
119
|
+
disabled_plugins=""
|
|
120
|
+
if [ -n "$PLUGIN_LIST_CACHE" ]; then
|
|
121
|
+
disabled_plugins=$(echo "$PLUGIN_LIST_CACHE" | python3 -c "
|
|
122
|
+
import sys
|
|
123
|
+
lines = sys.stdin.read().split('\n')
|
|
124
|
+
current_name = None
|
|
125
|
+
for line in lines:
|
|
126
|
+
line = line.strip()
|
|
127
|
+
if line.startswith('\u276f'):
|
|
128
|
+
full = line.split('\u276f')[1].strip()
|
|
129
|
+
if '@' in full:
|
|
130
|
+
current_name = full.split('@')[1]
|
|
131
|
+
else:
|
|
132
|
+
current_name = full
|
|
133
|
+
elif 'disabled' in line.lower() and current_name:
|
|
134
|
+
print(current_name)
|
|
135
|
+
current_name = None
|
|
136
|
+
elif 'enabled' in line.lower():
|
|
137
|
+
current_name = None
|
|
138
|
+
" 2>/dev/null || true)
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
if [ -d "$PLUGINS_DIR" ]; then
|
|
142
|
+
for plugin_dir in "$PLUGINS_DIR"/*/; do
|
|
143
|
+
[ -d "$plugin_dir" ] || continue
|
|
144
|
+
pname=$(basename "$plugin_dir")
|
|
145
|
+
# Skip temp directories
|
|
146
|
+
[[ "$pname" == temp_local_* ]] && continue
|
|
147
|
+
# Walk version subdirectories if they exist
|
|
148
|
+
count=$(find "$plugin_dir" -path "*/skills/*/SKILL.md" 2>/dev/null | wc -l | tr -d ' ')
|
|
149
|
+
if [ "$count" -gt 0 ]; then
|
|
150
|
+
# Check if this plugin is disabled
|
|
151
|
+
status="enabled"
|
|
152
|
+
echo "$disabled_plugins" | grep -qxF "$pname" 2>/dev/null && status="disabled"
|
|
153
|
+
echo "PLUGIN:${pname}:${count}:${status}"
|
|
154
|
+
# Collect skill names for duplicate detection
|
|
155
|
+
find "$plugin_dir" -path "*/skills/*/SKILL.md" -exec bash -c 'basename "$(dirname "$1")"' _ {} \; 2>/dev/null | while read -r sname; do
|
|
156
|
+
echo "PLUGIN_SKILL:${pname}:${sname}"
|
|
157
|
+
done
|
|
158
|
+
fi
|
|
159
|
+
done
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
echo ""
|
|
163
|
+
echo "=== CLAUDE_MD ==="
|
|
164
|
+
if [ -f "${CLAUDE_DIR}/CLAUDE.md" ]; then
|
|
165
|
+
size=$(wc -c < "${CLAUDE_DIR}/CLAUDE.md" | tr -d ' ')
|
|
166
|
+
echo "CLAUDE_MD:${size}"
|
|
167
|
+
# Section-level breakdown by top-level markdown headers
|
|
168
|
+
python3 -c "
|
|
169
|
+
import sys
|
|
170
|
+
|
|
171
|
+
with open(sys.argv[1]) as f:
|
|
172
|
+
lines = f.readlines()
|
|
173
|
+
|
|
174
|
+
sections = []
|
|
175
|
+
current_name = None
|
|
176
|
+
current_bytes = 0
|
|
177
|
+
|
|
178
|
+
for line in lines:
|
|
179
|
+
if line.startswith('# '):
|
|
180
|
+
if current_name is not None:
|
|
181
|
+
sections.append((current_name, current_bytes))
|
|
182
|
+
elif current_bytes > 0:
|
|
183
|
+
sections.append(('(preamble)', current_bytes))
|
|
184
|
+
current_name = line[2:].strip()[:60]
|
|
185
|
+
current_bytes = len(line.encode('utf-8'))
|
|
186
|
+
else:
|
|
187
|
+
current_bytes += len(line.encode('utf-8'))
|
|
188
|
+
|
|
189
|
+
if current_name is not None:
|
|
190
|
+
sections.append((current_name, current_bytes))
|
|
191
|
+
|
|
192
|
+
for name, size in sections:
|
|
193
|
+
print(f'CLAUDE_MD_SECTION:{name}:{size}')
|
|
194
|
+
" "${CLAUDE_DIR}/CLAUDE.md" 2>/dev/null || true
|
|
195
|
+
else
|
|
196
|
+
echo "CLAUDE_MD:0"
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
echo ""
|
|
200
|
+
echo "=== MEMORY FILES ==="
|
|
201
|
+
mem_total=0
|
|
202
|
+
mem_count=0
|
|
203
|
+
if [ -d "$PROJECTS_DIR" ]; then
|
|
204
|
+
while IFS= read -r f; do
|
|
205
|
+
size=$(wc -c < "$f" | tr -d ' ')
|
|
206
|
+
project=$(echo "$f" | sed "s|${PROJECTS_DIR}/||" | cut -d/ -f1)
|
|
207
|
+
fname=$(basename "$f")
|
|
208
|
+
echo "MEMORY:${project}:${fname}:${size}"
|
|
209
|
+
mem_total=$((mem_total + size))
|
|
210
|
+
mem_count=$((mem_count + 1))
|
|
211
|
+
done < <(find "$PROJECTS_DIR" -path "*/memory/*.md" -type f 2>/dev/null)
|
|
212
|
+
fi
|
|
213
|
+
echo "MEMORY_SUMMARY:${mem_count}:${mem_total}"
|
|
214
|
+
|
|
215
|
+
echo ""
|
|
216
|
+
echo "=== MCP SERVERS ==="
|
|
217
|
+
if [ -f "${CLAUDE_DIR}/settings.json" ]; then
|
|
218
|
+
python3 -c "
|
|
219
|
+
import json, sys
|
|
220
|
+
with open(sys.argv[1]) as f:
|
|
221
|
+
d = json.load(f)
|
|
222
|
+
servers = d.get('mcpServers', {})
|
|
223
|
+
print(f'MCP:{len(servers)}')
|
|
224
|
+
for name in sorted(servers.keys()):
|
|
225
|
+
print(f'MCP_SERVER:{name}')
|
|
226
|
+
" "${CLAUDE_DIR}/settings.json" 2>/dev/null || echo "MCP:0"
|
|
227
|
+
else
|
|
228
|
+
echo "MCP:0"
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
echo ""
|
|
232
|
+
echo "=== ISSUES ==="
|
|
233
|
+
|
|
234
|
+
# Broken symlinks (scoped to SKILL.md, covers top-level and nested)
|
|
235
|
+
[ -d "$SKILLS_DIR" ] && find "$SKILLS_DIR" -name "SKILL.md" -type l ! -exec test -e {} \; -print 2>/dev/null | while read -r link; do
|
|
236
|
+
dir=$(dirname "$link")
|
|
237
|
+
relpath=$(python3 -c "import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))" "$dir" "$SKILLS_DIR" 2>/dev/null || basename "$dir")
|
|
238
|
+
target=$(readlink "$link" 2>/dev/null || echo "unknown")
|
|
239
|
+
echo "ISSUE:broken_symlink:${relpath}:${target}"
|
|
240
|
+
done
|
|
241
|
+
|
|
242
|
+
# Duplicate skills (local name exists in plugin) — top-level + nested
|
|
243
|
+
if [ -d "$SKILLS_DIR" ] && [ -d "$PLUGINS_DIR" ]; then
|
|
244
|
+
plugin_skill_names=$(find "$PLUGINS_DIR" -path "*/skills/*/SKILL.md" -exec bash -c 'basename "$(dirname "$1")"' _ {} \; 2>/dev/null | sort -u)
|
|
245
|
+
# Top-level
|
|
246
|
+
for dir in "$SKILLS_DIR"/*/; do
|
|
247
|
+
[ -d "$dir" ] || continue
|
|
248
|
+
name=$(basename "$dir")
|
|
249
|
+
[ -f "${dir}SKILL.md" ] || continue
|
|
250
|
+
echo "$plugin_skill_names" | grep -qxF "$name" 2>/dev/null && echo "ISSUE:duplicate:${name}:local+plugin"
|
|
251
|
+
done
|
|
252
|
+
# Nested
|
|
253
|
+
for dir in "$SKILLS_DIR"/*/*/; do
|
|
254
|
+
[ -d "$dir" ] || continue
|
|
255
|
+
[ -f "${dir}SKILL.md" ] || continue
|
|
256
|
+
name=$(basename "$dir")
|
|
257
|
+
echo "$plugin_skill_names" | grep -qxF "$name" 2>/dev/null && echo "ISSUE:duplicate:$(basename "$(dirname "$dir")")/${name}:local+plugin"
|
|
258
|
+
done
|
|
259
|
+
fi
|
|
260
|
+
|
|
261
|
+
# Empty/template skills — top-level + nested
|
|
262
|
+
if [ -d "$SKILLS_DIR" ]; then
|
|
263
|
+
for dir in "$SKILLS_DIR"/*/; do
|
|
264
|
+
[ -d "$dir" ] || continue
|
|
265
|
+
if [ -f "${dir}SKILL.md" ] && grep -q "Replace with description" "${dir}SKILL.md" 2>/dev/null; then
|
|
266
|
+
echo "ISSUE:template:$(basename "$dir")"
|
|
267
|
+
fi
|
|
268
|
+
done
|
|
269
|
+
for dir in "$SKILLS_DIR"/*/*/; do
|
|
270
|
+
[ -d "$dir" ] || continue
|
|
271
|
+
if [ -f "${dir}SKILL.md" ] && grep -q "Replace with description" "${dir}SKILL.md" 2>/dev/null; then
|
|
272
|
+
echo "ISSUE:template:$(basename "$(dirname "$dir")")/$(basename "$dir")"
|
|
273
|
+
fi
|
|
274
|
+
done
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
# .skill duplicate directories
|
|
278
|
+
[ -d "$SKILLS_DIR" ] && for dir in "$SKILLS_DIR"/*.skill/; do
|
|
279
|
+
[ -d "$dir" ] || continue
|
|
280
|
+
base=$(basename "$dir" .skill)
|
|
281
|
+
[ -d "${SKILLS_DIR}/${base}" ] && echo "ISSUE:skill_dup:${base}"
|
|
282
|
+
done
|
|
283
|
+
|
|
284
|
+
# Oversized skill files (>10KB) — top-level + nested
|
|
285
|
+
if [ -d "$SKILLS_DIR" ]; then
|
|
286
|
+
for dir in "$SKILLS_DIR"/*/; do
|
|
287
|
+
[ -d "$dir" ] || continue
|
|
288
|
+
if [ -f "${dir}SKILL.md" ]; then
|
|
289
|
+
name=$(basename "$dir")
|
|
290
|
+
size=$(wc -c < "${dir}SKILL.md" | tr -d ' ')
|
|
291
|
+
[ "$size" -gt 10240 ] && echo "ISSUE:oversized_skill:${name}:${size}"
|
|
292
|
+
fi
|
|
293
|
+
done
|
|
294
|
+
for dir in "$SKILLS_DIR"/*/*/; do
|
|
295
|
+
[ -d "$dir" ] || continue
|
|
296
|
+
if [ -f "${dir}SKILL.md" ]; then
|
|
297
|
+
name="$(basename "$(dirname "$dir")")/$(basename "$dir")"
|
|
298
|
+
size=$(wc -c < "${dir}SKILL.md" | tr -d ' ')
|
|
299
|
+
[ "$size" -gt 10240 ] && echo "ISSUE:oversized_skill:${name}:${size}"
|
|
300
|
+
fi
|
|
301
|
+
done
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
# Oversized memory files (>5KB)
|
|
305
|
+
[ -d "$PROJECTS_DIR" ] && find "$PROJECTS_DIR" -path "*/memory/*.md" -type f -size +5k 2>/dev/null | while read -r f; do
|
|
306
|
+
size=$(wc -c < "$f" | tr -d ' ')
|
|
307
|
+
project=$(echo "$f" | sed "s|${PROJECTS_DIR}/||" | cut -d/ -f1)
|
|
308
|
+
fname=$(basename "$f")
|
|
309
|
+
echo "ISSUE:oversized_memory:${project}/${fname}:${size}"
|
|
310
|
+
done
|
|
311
|
+
|
|
312
|
+
# Stale project memory (no files modified in 90+ days)
|
|
313
|
+
if [ -d "$PROJECTS_DIR" ]; then
|
|
314
|
+
for proj_dir in "$PROJECTS_DIR"/*/; do
|
|
315
|
+
[ -d "${proj_dir}memory" ] || continue
|
|
316
|
+
project=$(basename "$proj_dir")
|
|
317
|
+
# Quick check: any file modified in last 90 days? (maxdepth 1 to match glob below)
|
|
318
|
+
recent=$(find "${proj_dir}memory" -maxdepth 1 -type f -name "*.md" -mtime -90 2>/dev/null | wc -l | tr -d ' ')
|
|
319
|
+
if [ "$recent" -eq 0 ]; then
|
|
320
|
+
# All files older than 90 days — get details via python3
|
|
321
|
+
detail=$(python3 -c "
|
|
322
|
+
import os, sys, glob, time
|
|
323
|
+
files = glob.glob(sys.argv[1] + '/*.md')
|
|
324
|
+
if files:
|
|
325
|
+
newest = max(os.path.getmtime(f) for f in files)
|
|
326
|
+
days = int((time.time() - newest) / 86400)
|
|
327
|
+
total = sum(os.path.getsize(f) for f in files)
|
|
328
|
+
print(f'{days}d,{len(files)}files,{total}bytes')
|
|
329
|
+
" "${proj_dir}memory" 2>/dev/null)
|
|
330
|
+
[ -n "$detail" ] && echo "ISSUE:stale_project:${project}:${detail}"
|
|
331
|
+
fi
|
|
332
|
+
done
|
|
333
|
+
fi
|
|
334
|
+
|
|
335
|
+
# Temp/orphaned plugin cache (failed installs)
|
|
336
|
+
[ -d "$PLUGINS_DIR" ] && for dir in "$PLUGINS_DIR"/temp_local_*/; do
|
|
337
|
+
[ -d "$dir" ] || continue
|
|
338
|
+
name=$(basename "$dir")
|
|
339
|
+
size=$(du -sk "$dir" 2>/dev/null | cut -f1)
|
|
340
|
+
echo "ISSUE:temp_cache:${name}:${size}KB"
|
|
341
|
+
done
|
|
342
|
+
|
|
343
|
+
# Disabled plugins (still in cache) — uses cached plugin list
|
|
344
|
+
if [ -n "$PLUGIN_LIST_CACHE" ]; then
|
|
345
|
+
echo "$PLUGIN_LIST_CACHE" | python3 -c "
|
|
346
|
+
import sys
|
|
347
|
+
lines = sys.stdin.read().split('\n')
|
|
348
|
+
current_name = None
|
|
349
|
+
for line in lines:
|
|
350
|
+
line = line.strip()
|
|
351
|
+
if line.startswith('\u276f'):
|
|
352
|
+
current_name = line.split('\u276f')[1].strip()
|
|
353
|
+
elif 'disabled' in line.lower() and current_name:
|
|
354
|
+
print(f'ISSUE:disabled_plugin:{current_name}')
|
|
355
|
+
current_name = None
|
|
356
|
+
elif 'enabled' in line.lower():
|
|
357
|
+
current_name = None
|
|
358
|
+
" 2>/dev/null || true
|
|
359
|
+
fi
|
|
360
|
+
|
|
361
|
+
echo ""
|
|
362
|
+
echo "=== DONE ==="
|