claude-slim 2.3.0 → 2.4.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/README.md +28 -11
- package/dist/cleaner.js +2 -1
- package/dist/cli.js +12 -6
- package/dist/scanner/detectors.d.ts +3 -0
- package/dist/scanner/detectors.js +32 -0
- package/dist/scanner/index.d.ts +4 -1
- package/dist/scanner/index.js +9 -2
- package/dist/scanner/sessions.d.ts +8 -0
- package/dist/scanner/sessions.js +150 -0
- package/dist/types.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ That's slower responses. Hitting your usage cap faster. Paying for context you'r
|
|
|
73
73
|
| Duplicates | Same skill registered from multiple sources |
|
|
74
74
|
| Empty templates | Placeholder skills with no content |
|
|
75
75
|
| Oversized files | SKILL.md over 10KB |
|
|
76
|
+
| **Unused skills** | **Local skills never invoked in your last N days of sessions (default 60d)** |
|
|
76
77
|
| Stale memory | Large memory files loaded every session |
|
|
77
78
|
| Disabled plugins | Installed but disabled plugins still in cache |
|
|
78
79
|
| Stale projects | Project memory untouched for 90+ days |
|
|
@@ -141,21 +142,23 @@ Then just type `/claude-slim` in any session.
|
|
|
141
142
|
## Usage
|
|
142
143
|
|
|
143
144
|
```bash
|
|
144
|
-
/claude-slim
|
|
145
|
-
/claude-slim scan
|
|
146
|
-
/claude-slim scan --json
|
|
147
|
-
/claude-slim
|
|
145
|
+
/claude-slim # Full pipeline: scan → propose → clean → report
|
|
146
|
+
/claude-slim scan # Report only, no changes
|
|
147
|
+
/claude-slim scan --json # Machine-readable JSON output
|
|
148
|
+
/claude-slim scan --lookback-days 30 # Treat skills idle for 30+ days as unused
|
|
149
|
+
/claude-slim restore # Bring back anything you disabled
|
|
148
150
|
```
|
|
149
151
|
|
|
150
152
|
CLI equivalents:
|
|
151
153
|
|
|
152
154
|
```bash
|
|
153
|
-
npx claude-slim clean
|
|
154
|
-
npx claude-slim clean --dry-run
|
|
155
|
-
npx claude-slim clean --auto
|
|
156
|
-
npx claude-slim
|
|
157
|
-
npx claude-slim
|
|
158
|
-
npx claude-slim
|
|
155
|
+
npx claude-slim clean # Full pipeline
|
|
156
|
+
npx claude-slim clean --dry-run # See what would happen (no changes)
|
|
157
|
+
npx claude-slim clean --auto # Non-interactive, Tier 1 only (CI/scripts)
|
|
158
|
+
npx claude-slim clean --lookback-days N # Tune the unused-skill detection window
|
|
159
|
+
npx claude-slim scan # Report only
|
|
160
|
+
npx claude-slim restore # Undo
|
|
161
|
+
npx claude-slim report # Show savings from last clean
|
|
159
162
|
```
|
|
160
163
|
|
|
161
164
|
---
|
|
@@ -212,6 +215,20 @@ From a real cleanup session:
|
|
|
212
215
|
|
|
213
216
|
---
|
|
214
217
|
|
|
218
|
+
## v2.4 — What's new
|
|
219
|
+
|
|
220
|
+
- **Unused-skill detection** — claude-slim now reads your `~/.claude/projects/*/*.jsonl` session transcripts, finds every `Skill` tool invocation in the last 60 days, and flags local skills you've installed but never actually used. Tier 3 (Optional, never auto-selected) so you decide. Configurable lookback via `--lookback-days <n>`. Falls back silently if there's not enough session history (≥3 sessions required) — no false-flagging when the data source is unreliable.
|
|
221
|
+
- **Plugin skills are intentionally out of scope** for this detector. They live inside `~/.claude/plugins/cache/` and are managed by the Claude Code plugin runtime; moving them would partially uninstall the plugin. Use `claude plugin disable <name>` for plugin-level cleanup.
|
|
222
|
+
- **Per-file session-usage cache** at `~/.claude/.skill-usage-cache.json` keyed by mtime. Warm rescans only re-parse session logs that have changed.
|
|
223
|
+
- **Node 20+** is now the engine floor (previously `>=18`, but Node 18 was already dropped from CI in v2.3.0).
|
|
224
|
+
|
|
225
|
+
## v2.3 — What's new
|
|
226
|
+
|
|
227
|
+
- **Detector registry refactor (v2.3.0)** — Scanner split from a 588-line module into focused detectors under `src/scanner/`. Adding a new heuristic is a one-function addition; see CONTRIBUTING.md. Public API unchanged.
|
|
228
|
+
- **Path-containment guard (v2.2.3)** — Every destructive op refuses any target path that escapes `~/.claude/`. `runCommand` no longer goes through a shell. `temp_cache` cleanup is symlink-safe.
|
|
229
|
+
- **Report sign fix (v2.2.3)** — The breakdown table's Saved column was inverted in earlier 2.2.x; cleanup now shows correct savings per row.
|
|
230
|
+
- **85 tests (was 73)** — New round-trip coverage for path containment, restore guards, breakdown sign, restore-selection dedup, atomic tokenizer flush, and custom detector injection.
|
|
231
|
+
|
|
215
232
|
## v2.2 — What's new
|
|
216
233
|
|
|
217
234
|
- **Atomic `stale_project` clean/restore** — Single directory `rename()` instead of per-file loop. No more partial-failure state if the operation is interrupted.
|
|
@@ -242,7 +259,7 @@ From a real cleanup session:
|
|
|
242
259
|
|
|
243
260
|
## Requirements
|
|
244
261
|
|
|
245
|
-
- Node.js
|
|
262
|
+
- Node.js 20+
|
|
246
263
|
- macOS or Linux
|
|
247
264
|
- Claude Code CLI
|
|
248
265
|
|
package/dist/cleaner.js
CHANGED
|
@@ -51,7 +51,8 @@ export async function cleanIssues(issues) {
|
|
|
51
51
|
else if (issue.type === 'template' ||
|
|
52
52
|
issue.type === 'duplicate' ||
|
|
53
53
|
issue.type === 'skill_dup' ||
|
|
54
|
-
issue.type === 'oversized_skill'
|
|
54
|
+
issue.type === 'oversized_skill' ||
|
|
55
|
+
issue.type === 'unused_skill') {
|
|
55
56
|
// Move skill directory to disabled — use name (not basename) to avoid namespace collisions
|
|
56
57
|
const safeName = issue.name.replace(/\//g, '--');
|
|
57
58
|
const dest = join(disabledDir, safeName);
|
package/dist/cli.js
CHANGED
|
@@ -22,9 +22,10 @@ program
|
|
|
22
22
|
.command('scan')
|
|
23
23
|
.description('Scan environment and report issues')
|
|
24
24
|
.option('--json', 'Output raw JSON')
|
|
25
|
+
.option('--lookback-days <n>', 'Days of session history for skill-usage analysis', '60')
|
|
25
26
|
.action(async (opts) => {
|
|
26
27
|
await initTokenizer();
|
|
27
|
-
const result = await scan();
|
|
28
|
+
const result = await scan({ lookbackDays: parseInt(opts.lookbackDays, 10) || 60 });
|
|
28
29
|
await flushCache();
|
|
29
30
|
if (opts.json) {
|
|
30
31
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -40,11 +41,13 @@ program
|
|
|
40
41
|
.option('--dry-run', 'Show what would happen without making changes')
|
|
41
42
|
.option('--auto', 'Non-interactive: auto-select Tier 1 items only')
|
|
42
43
|
.option('--sessions-per-day <n>', 'Sessions per day for savings estimate', '2')
|
|
44
|
+
.option('--lookback-days <n>', 'Days of session history for skill-usage analysis', '60')
|
|
43
45
|
.action(async (opts) => {
|
|
44
46
|
await runCleanPipeline({
|
|
45
47
|
dryRun: !!opts.dryRun,
|
|
46
48
|
auto: !!opts.auto,
|
|
47
49
|
sessionsPerDay: parseInt(opts.sessionsPerDay, 10) || 2,
|
|
50
|
+
lookbackDays: parseInt(opts.lookbackDays, 10) || 60,
|
|
48
51
|
});
|
|
49
52
|
});
|
|
50
53
|
// --- restore ---
|
|
@@ -110,9 +113,10 @@ program
|
|
|
110
113
|
.command('report')
|
|
111
114
|
.description('Show savings report from last clean')
|
|
112
115
|
.option('--sessions-per-day <n>', 'Sessions per day for savings estimate', '2')
|
|
116
|
+
.option('--lookback-days <n>', 'Days of session history for skill-usage analysis', '60')
|
|
113
117
|
.action(async (opts) => {
|
|
114
118
|
await initTokenizer();
|
|
115
|
-
const result = await scan();
|
|
119
|
+
const result = await scan({ lookbackDays: parseInt(opts.lookbackDays, 10) || 60 });
|
|
116
120
|
const entries = await readManifest();
|
|
117
121
|
const movedEntries = entries.filter((e) => e.tokenCount && e.tokenCount > 0);
|
|
118
122
|
if (movedEntries.length === 0) {
|
|
@@ -125,7 +129,9 @@ program
|
|
|
125
129
|
// Only skill-type entries contributed to the per-skill prompt overhead
|
|
126
130
|
// (stale_project restores memory tokens separately; broken_symlink/
|
|
127
131
|
// temp_cache never counted toward totalTokensBefore).
|
|
128
|
-
const SKILL_TYPES = new Set([
|
|
132
|
+
const SKILL_TYPES = new Set([
|
|
133
|
+
'template', 'duplicate', 'skill_dup', 'oversized_skill', 'unused_skill',
|
|
134
|
+
]);
|
|
129
135
|
const removedSkillEntries = movedEntries.filter((e) => SKILL_TYPES.has(e.type));
|
|
130
136
|
const removedMemoryTokens = movedEntries
|
|
131
137
|
.filter((e) => e.type === 'stale_project')
|
|
@@ -155,12 +161,12 @@ program
|
|
|
155
161
|
});
|
|
156
162
|
// --- default (no subcommand) → run clean ---
|
|
157
163
|
program.action(async () => {
|
|
158
|
-
await runCleanPipeline({ dryRun: false, auto: false, sessionsPerDay: 2 });
|
|
164
|
+
await runCleanPipeline({ dryRun: false, auto: false, sessionsPerDay: 2, lookbackDays: 60 });
|
|
159
165
|
});
|
|
160
166
|
// --- shared clean pipeline ---
|
|
161
167
|
async function runCleanPipeline(opts) {
|
|
162
168
|
await initTokenizer();
|
|
163
|
-
const result = await scan();
|
|
169
|
+
const result = await scan({ lookbackDays: opts.lookbackDays });
|
|
164
170
|
if (result.issues.length === 0) {
|
|
165
171
|
console.log('\n \x1b[32mAlready slim!\x1b[0m No issues found.\n');
|
|
166
172
|
await flushCache();
|
|
@@ -207,7 +213,7 @@ async function runCleanPipeline(opts) {
|
|
|
207
213
|
console.log('');
|
|
208
214
|
const cleanResult = await cleanIssues(selectedIssues);
|
|
209
215
|
// Re-scan after cleanup for accurate breakdown
|
|
210
|
-
const afterResult = await scan();
|
|
216
|
+
const afterResult = await scan({ lookbackDays: opts.lookbackDays });
|
|
211
217
|
const reportData = calculateReport(result, afterResult, cleanResult.moved, opts.sessionsPerDay);
|
|
212
218
|
console.log('');
|
|
213
219
|
console.log(formatReportBox(reportData));
|
|
@@ -11,6 +11,9 @@ export interface DetectorContext {
|
|
|
11
11
|
disabledPlugins: Set<string>;
|
|
12
12
|
plugins: PluginInfo[];
|
|
13
13
|
contents: Map<string, string>;
|
|
14
|
+
recentSkillInvocations: Set<string>;
|
|
15
|
+
sessionDataAvailable: boolean;
|
|
16
|
+
lookbackDays: number;
|
|
14
17
|
}
|
|
15
18
|
export interface Detector {
|
|
16
19
|
name: string;
|
|
@@ -144,6 +144,37 @@ const staleProjectDetector = {
|
|
|
144
144
|
});
|
|
145
145
|
},
|
|
146
146
|
};
|
|
147
|
+
const unusedSkillDetector = {
|
|
148
|
+
name: 'unused_skill',
|
|
149
|
+
detect({ localSkills, recentSkillInvocations, sessionDataAvailable, lookbackDays, }) {
|
|
150
|
+
// Suppress entirely when the data source is unreliable — better no signal
|
|
151
|
+
// than a wrong one that flags every skill as unused.
|
|
152
|
+
if (!sessionDataAvailable)
|
|
153
|
+
return [];
|
|
154
|
+
const issues = [];
|
|
155
|
+
for (const skill of localSkills) {
|
|
156
|
+
// Direct hit: invocation set contains the skill name as-is.
|
|
157
|
+
if (recentSkillInvocations.has(skill.name))
|
|
158
|
+
continue;
|
|
159
|
+
// Nested skill (e.g. "org/ship"): also check the bare leaf name, which
|
|
160
|
+
// is how it would appear in a Skill tool_use input.
|
|
161
|
+
if (skill.name.includes('/')) {
|
|
162
|
+
const leaf = skill.name.split('/').pop();
|
|
163
|
+
if (recentSkillInvocations.has(leaf))
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
issues.push({
|
|
167
|
+
type: 'unused_skill',
|
|
168
|
+
tier: 3,
|
|
169
|
+
name: skill.name,
|
|
170
|
+
detail: `not invoked in ${lookbackDays}d`,
|
|
171
|
+
tokens: skill.tokens,
|
|
172
|
+
path: skill.path,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return issues;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
147
178
|
const disabledPluginDetector = {
|
|
148
179
|
name: 'disabled_plugin',
|
|
149
180
|
detect({ plugins, disabledPlugins }) {
|
|
@@ -174,6 +205,7 @@ export const detectors = [
|
|
|
174
205
|
tempCacheDetector,
|
|
175
206
|
oversizedMemoryDetector,
|
|
176
207
|
staleProjectDetector,
|
|
208
|
+
unusedSkillDetector,
|
|
177
209
|
disabledPluginDetector,
|
|
178
210
|
];
|
|
179
211
|
export function classifyIssues(ctx, registry = detectors) {
|
package/dist/scanner/index.d.ts
CHANGED
package/dist/scanner/index.js
CHANGED
|
@@ -8,15 +8,19 @@ import { scanMemoryFiles } from './memory.js';
|
|
|
8
8
|
import { scanMcpServers } from './mcp.js';
|
|
9
9
|
import { parseClaudeMdSections } from './claude-md.js';
|
|
10
10
|
import { getDisabledPlugins } from './disabled-plugins.js';
|
|
11
|
+
import { scanSessionUsage } from './sessions.js';
|
|
11
12
|
import { classifyIssues } from './detectors.js';
|
|
12
13
|
import { SKILL_PROMPT_OVERHEAD_TOKENS } from './constants.js';
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
const DEFAULT_LOOKBACK_DAYS = 60;
|
|
15
|
+
export async function scan(opts = {}) {
|
|
16
|
+
const lookbackDays = opts.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;
|
|
17
|
+
const [{ skills: localSkills, brokenSymlinks, contents }, { skills: pluginSkills, plugins, tempCaches }, { memoryFiles, staleProjects }, mcp, disabledPlugins, sessionUsage,] = await Promise.all([
|
|
15
18
|
scanLocalSkills(),
|
|
16
19
|
scanPluginSkills(),
|
|
17
20
|
scanMemoryFiles(),
|
|
18
21
|
scanMcpServers(),
|
|
19
22
|
getDisabledPlugins(),
|
|
23
|
+
scanSessionUsage(lookbackDays),
|
|
20
24
|
]);
|
|
21
25
|
// Annotate plugin status
|
|
22
26
|
for (const plugin of plugins) {
|
|
@@ -33,6 +37,9 @@ export async function scan() {
|
|
|
33
37
|
localSkills, pluginSkills, brokenSymlinks, memoryFiles,
|
|
34
38
|
tempCaches, staleProjects, disabledPlugins, plugins,
|
|
35
39
|
contents,
|
|
40
|
+
recentSkillInvocations: sessionUsage.invokedSkills,
|
|
41
|
+
sessionDataAvailable: sessionUsage.dataAvailable,
|
|
42
|
+
lookbackDays,
|
|
36
43
|
});
|
|
37
44
|
// Estimate total tokens at startup
|
|
38
45
|
const skillListingTokens = (localSkills.length + pluginSkills.length) * SKILL_PROMPT_OVERHEAD_TOKENS;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SessionScanResult {
|
|
2
|
+
invokedSkills: Set<string>;
|
|
3
|
+
dataAvailable: boolean;
|
|
4
|
+
sessionsScanned: number;
|
|
5
|
+
sessionsInWindow: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function extractSkillsFromTranscript(content: string): string[];
|
|
8
|
+
export declare function scanSessionUsage(lookbackDays: number): Promise<SessionScanResult>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readFile, readdir, stat, writeFile, mkdir, rename } from 'node:fs/promises';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { getClaudeDir } from '../paths.js';
|
|
4
|
+
const CACHE_VERSION = 1;
|
|
5
|
+
// Below this many sessions in the lookback window we suppress unused-skill
|
|
6
|
+
// classification — too little signal, "unused" would be misleading.
|
|
7
|
+
const MIN_SESSIONS_FOR_DATA_AVAILABLE = 3;
|
|
8
|
+
function getCachePath() {
|
|
9
|
+
return join(getClaudeDir(), '.skill-usage-cache.json');
|
|
10
|
+
}
|
|
11
|
+
async function loadCache() {
|
|
12
|
+
try {
|
|
13
|
+
const raw = await readFile(getCachePath(), 'utf-8');
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
if (parsed.version !== CACHE_VERSION)
|
|
16
|
+
return { version: CACHE_VERSION, entries: {} };
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return { version: CACHE_VERSION, entries: {} };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function saveCache(cache) {
|
|
24
|
+
const target = getCachePath();
|
|
25
|
+
const tmp = target + '.tmp';
|
|
26
|
+
try {
|
|
27
|
+
await mkdir(dirname(target), { recursive: true });
|
|
28
|
+
// Atomic write — same pattern as token cache. A crash mid-flush leaves the
|
|
29
|
+
// prior cache intact rather than a torn JSON file.
|
|
30
|
+
await writeFile(tmp, JSON.stringify(cache));
|
|
31
|
+
await rename(tmp, target);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Non-critical: missing cache just means a slower next scan.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Extract Skill-tool invocations from a single JSONL session log.
|
|
38
|
+
// Each line in `~/.claude/projects/<slug>/<sessionId>.jsonl` is a JSON event;
|
|
39
|
+
// we look for `message.content[]` entries shaped
|
|
40
|
+
// { type: 'tool_use', name: 'Skill', input: { skill: '<id>' } }
|
|
41
|
+
// and collect the `skill` strings (e.g. 'superpowers:brainstorming').
|
|
42
|
+
//
|
|
43
|
+
// Schema-defensive: any line/field that does not match is silently skipped,
|
|
44
|
+
// so a partial schema change degrades gracefully rather than throwing.
|
|
45
|
+
export function extractSkillsFromTranscript(content) {
|
|
46
|
+
const skills = [];
|
|
47
|
+
const lines = content.split('\n');
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
if (!line)
|
|
50
|
+
continue;
|
|
51
|
+
let obj;
|
|
52
|
+
try {
|
|
53
|
+
obj = JSON.parse(line);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (typeof obj !== 'object' || obj === null)
|
|
59
|
+
continue;
|
|
60
|
+
const message = obj.message;
|
|
61
|
+
if (typeof message !== 'object' || message === null)
|
|
62
|
+
continue;
|
|
63
|
+
const msgContent = message.content;
|
|
64
|
+
if (!Array.isArray(msgContent))
|
|
65
|
+
continue;
|
|
66
|
+
for (const c of msgContent) {
|
|
67
|
+
if (typeof c !== 'object' || c === null)
|
|
68
|
+
continue;
|
|
69
|
+
const rec = c;
|
|
70
|
+
if (rec.type !== 'tool_use' || rec.name !== 'Skill')
|
|
71
|
+
continue;
|
|
72
|
+
const input = rec.input;
|
|
73
|
+
if (typeof input !== 'object' || input === null)
|
|
74
|
+
continue;
|
|
75
|
+
const skill = input.skill;
|
|
76
|
+
if (typeof skill === 'string')
|
|
77
|
+
skills.push(skill);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return skills;
|
|
81
|
+
}
|
|
82
|
+
// Walk every `~/.claude/projects/<slug>/*.jsonl` whose mtime falls inside the
|
|
83
|
+
// lookback window. Per-file results are cached by mtime, so warm scans only
|
|
84
|
+
// re-read files that have changed.
|
|
85
|
+
export async function scanSessionUsage(lookbackDays) {
|
|
86
|
+
const projectsDir = join(getClaudeDir(), 'projects');
|
|
87
|
+
const cutoffMs = Date.now() - lookbackDays * 24 * 60 * 60 * 1000;
|
|
88
|
+
const cache = await loadCache();
|
|
89
|
+
// Pruned cache: only entries seen this scan survive. Keeps the file bounded
|
|
90
|
+
// across many scans even as old session logs get rotated/deleted.
|
|
91
|
+
const newEntries = {};
|
|
92
|
+
const invokedSkills = new Set();
|
|
93
|
+
let sessionsScanned = 0;
|
|
94
|
+
let sessionsInWindow = 0;
|
|
95
|
+
let projectDirs = [];
|
|
96
|
+
try {
|
|
97
|
+
projectDirs = await readdir(projectsDir);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return { invokedSkills, dataAvailable: false, sessionsScanned: 0, sessionsInWindow: 0 };
|
|
101
|
+
}
|
|
102
|
+
for (const projectName of projectDirs) {
|
|
103
|
+
const projectPath = join(projectsDir, projectName);
|
|
104
|
+
let entries = [];
|
|
105
|
+
try {
|
|
106
|
+
entries = await readdir(projectPath);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
if (!entry.endsWith('.jsonl'))
|
|
113
|
+
continue;
|
|
114
|
+
const filePath = join(projectPath, entry);
|
|
115
|
+
let mtimeMs;
|
|
116
|
+
try {
|
|
117
|
+
mtimeMs = (await stat(filePath)).mtimeMs;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
sessionsScanned++;
|
|
123
|
+
if (mtimeMs < cutoffMs)
|
|
124
|
+
continue;
|
|
125
|
+
sessionsInWindow++;
|
|
126
|
+
// Cache hit: reuse the parsed skill list, no I/O on the file body.
|
|
127
|
+
const cached = cache.entries[filePath];
|
|
128
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
129
|
+
for (const s of cached.skills)
|
|
130
|
+
invokedSkills.add(s);
|
|
131
|
+
newEntries[filePath] = cached;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
let content;
|
|
135
|
+
try {
|
|
136
|
+
content = await readFile(filePath, 'utf-8');
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const skills = extractSkillsFromTranscript(content);
|
|
142
|
+
for (const s of skills)
|
|
143
|
+
invokedSkills.add(s);
|
|
144
|
+
newEntries[filePath] = { mtimeMs, skills: Array.from(new Set(skills)) };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const dataAvailable = sessionsInWindow >= MIN_SESSIONS_FOR_DATA_AVAILABLE && invokedSkills.size > 0;
|
|
148
|
+
await saveCache({ version: CACHE_VERSION, entries: newEntries });
|
|
149
|
+
return { invokedSkills, dataAvailable, sessionsScanned, sessionsInWindow };
|
|
150
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface PluginInfo {
|
|
|
25
25
|
status?: 'enabled' | 'disabled';
|
|
26
26
|
}
|
|
27
27
|
export type IssueTier = 1 | 2 | 3;
|
|
28
|
-
export type IssueType = 'broken_symlink' | 'template' | 'skill_dup' | 'duplicate' | 'oversized_memory' | 'oversized_skill' | 'disabled_plugin' | 'stale_project' | 'temp_cache';
|
|
28
|
+
export type IssueType = 'broken_symlink' | 'template' | 'skill_dup' | 'duplicate' | 'oversized_memory' | 'oversized_skill' | 'unused_skill' | 'disabled_plugin' | 'stale_project' | 'temp_cache';
|
|
29
29
|
export interface Issue {
|
|
30
30
|
type: IssueType;
|
|
31
31
|
tier: IssueTier;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-slim",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Analyze and reduce Claude Code token overhead",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"url": "https://github.com/iops-leo/claude-slim/issues"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
|
-
"node": ">=
|
|
42
|
+
"node": ">=20"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"commander": "^13.0.0",
|