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/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 ==="