claude-slim 2.2.3 → 2.3.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 +25 -8
- package/dist/scanner/claude-md.d.ts +5 -0
- package/dist/scanner/claude-md.js +45 -0
- package/dist/scanner/constants.d.ts +4 -0
- package/dist/scanner/constants.js +4 -0
- package/dist/scanner/detectors.d.ts +20 -0
- package/dist/scanner/detectors.js +183 -0
- package/dist/scanner/disabled-plugins.d.ts +2 -0
- package/dist/scanner/disabled-plugins.js +26 -0
- package/dist/scanner/fs-walk.d.ts +7 -0
- package/dist/scanner/fs-walk.js +82 -0
- package/dist/scanner/index.d.ts +2 -0
- package/dist/scanner/index.js +55 -0
- package/dist/scanner/local-skills.d.ts +12 -0
- package/dist/scanner/local-skills.js +97 -0
- package/dist/scanner/mcp.d.ts +5 -0
- package/dist/scanner/mcp.js +17 -0
- package/dist/scanner/memory.d.ts +13 -0
- package/dist/scanner/memory.js +50 -0
- package/dist/scanner/plugin-skills.d.ts +12 -0
- package/dist/scanner/plugin-skills.js +65 -0
- package/dist/scanner.d.ts +5 -15
- package/dist/scanner.js +8 -530
- package/package.json +5 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { countTokensCached } from '../tokenizer.js';
|
|
4
|
+
import { getProjectsDir } from '../paths.js';
|
|
5
|
+
import { safeReadFile, safeReaddir } from './fs-walk.js';
|
|
6
|
+
import { STALE_DAYS } from './constants.js';
|
|
7
|
+
export async function scanMemoryFiles() {
|
|
8
|
+
const memoryFiles = [];
|
|
9
|
+
const staleProjects = [];
|
|
10
|
+
const projectsDir = getProjectsDir();
|
|
11
|
+
const projectDirs = await safeReaddir(projectsDir);
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const scanPromises = projectDirs.map(async (project) => {
|
|
14
|
+
const memDir = join(projectsDir, project, 'memory');
|
|
15
|
+
const files = await safeReaddir(memDir);
|
|
16
|
+
const mdFiles = files.filter((f) => f.endsWith('.md'));
|
|
17
|
+
let newestMtime = 0;
|
|
18
|
+
let totalBytes = 0;
|
|
19
|
+
for (const file of mdFiles) {
|
|
20
|
+
const filePath = join(memDir, file);
|
|
21
|
+
const content = await safeReadFile(filePath);
|
|
22
|
+
if (content !== null) {
|
|
23
|
+
const sizeBytes = Buffer.byteLength(content);
|
|
24
|
+
memoryFiles.push({
|
|
25
|
+
project,
|
|
26
|
+
name: file,
|
|
27
|
+
path: filePath,
|
|
28
|
+
sizeBytes,
|
|
29
|
+
tokens: countTokensCached(content, filePath),
|
|
30
|
+
});
|
|
31
|
+
totalBytes += sizeBytes;
|
|
32
|
+
try {
|
|
33
|
+
const s = await stat(filePath);
|
|
34
|
+
if (s.mtimeMs > newestMtime)
|
|
35
|
+
newestMtime = s.mtimeMs;
|
|
36
|
+
}
|
|
37
|
+
catch { /* skip */ }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Check for stale project (no files modified in 90+ days)
|
|
41
|
+
if (mdFiles.length > 0 && newestMtime > 0) {
|
|
42
|
+
const ageDays = Math.floor((now - newestMtime) / (1000 * 60 * 60 * 24));
|
|
43
|
+
if (ageDays > STALE_DAYS) {
|
|
44
|
+
staleProjects.push({ project, path: memDir, ageDays, fileCount: mdFiles.length, totalBytes });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
await Promise.all(scanPromises);
|
|
49
|
+
return { memoryFiles, staleProjects };
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SkillInfo, PluginInfo } from '../types.js';
|
|
2
|
+
export interface TempCache {
|
|
3
|
+
name: string;
|
|
4
|
+
path: string;
|
|
5
|
+
sizeKB: number;
|
|
6
|
+
}
|
|
7
|
+
export interface PluginSkillsResult {
|
|
8
|
+
skills: SkillInfo[];
|
|
9
|
+
plugins: PluginInfo[];
|
|
10
|
+
tempCaches: TempCache[];
|
|
11
|
+
}
|
|
12
|
+
export declare function scanPluginSkills(): Promise<PluginSkillsResult>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { countTokensCached } from '../tokenizer.js';
|
|
3
|
+
import { getPluginsDir } from '../paths.js';
|
|
4
|
+
import { safeReadFile, safeReaddir, isDirectory, getDirSize } from './fs-walk.js';
|
|
5
|
+
export async function scanPluginSkills() {
|
|
6
|
+
const skills = [];
|
|
7
|
+
const plugins = [];
|
|
8
|
+
const tempCaches = [];
|
|
9
|
+
const pluginsDir = getPluginsDir();
|
|
10
|
+
const pluginDirs = await safeReaddir(pluginsDir);
|
|
11
|
+
const scanPromises = pluginDirs.map(async (pluginName) => {
|
|
12
|
+
const pluginDir = join(pluginsDir, pluginName);
|
|
13
|
+
if (!(await isDirectory(pluginDir)))
|
|
14
|
+
return;
|
|
15
|
+
// Detect temp_local_* cache dirs (failed plugin installs)
|
|
16
|
+
if (pluginName.startsWith('temp_local_')) {
|
|
17
|
+
const size = await getDirSize(pluginDir);
|
|
18
|
+
tempCaches.push({ name: pluginName, path: pluginDir, sizeKB: Math.round(size / 1024) });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const pluginSkillNames = [];
|
|
22
|
+
const walkDir = async (dir) => {
|
|
23
|
+
const entries = await safeReaddir(dir);
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const entryPath = join(dir, entry);
|
|
26
|
+
if (!(await isDirectory(entryPath)))
|
|
27
|
+
continue;
|
|
28
|
+
if (entry === 'skills') {
|
|
29
|
+
const skillDirs = await safeReaddir(entryPath);
|
|
30
|
+
for (const skillDir of skillDirs) {
|
|
31
|
+
const skillPath = join(entryPath, skillDir);
|
|
32
|
+
if (!(await isDirectory(skillPath)))
|
|
33
|
+
continue;
|
|
34
|
+
const skillMd = join(skillPath, 'SKILL.md');
|
|
35
|
+
const content = await safeReadFile(skillMd);
|
|
36
|
+
if (content !== null) {
|
|
37
|
+
pluginSkillNames.push(skillDir);
|
|
38
|
+
skills.push({
|
|
39
|
+
name: skillDir,
|
|
40
|
+
path: skillPath,
|
|
41
|
+
sizeBytes: Buffer.byteLength(content),
|
|
42
|
+
tokens: countTokensCached(content, skillMd),
|
|
43
|
+
source: 'plugin',
|
|
44
|
+
pluginName,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
await walkDir(entryPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
await walkDir(pluginDir);
|
|
55
|
+
if (pluginSkillNames.length > 0) {
|
|
56
|
+
plugins.push({
|
|
57
|
+
name: pluginName,
|
|
58
|
+
skillCount: pluginSkillNames.length,
|
|
59
|
+
skills: pluginSkillNames,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
await Promise.all(scanPromises);
|
|
64
|
+
return { skills, plugins, tempCaches };
|
|
65
|
+
}
|
package/dist/scanner.d.ts
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
export declare function dedupeBySymlink(candidates: SkillCandidate[]): SkillInfo[];
|
|
8
|
-
export declare function parseDisabledPlugins(output: string): Set<string>;
|
|
9
|
-
export declare function parseClaudeMdSections(content: string): Array<{
|
|
10
|
-
name: string;
|
|
11
|
-
sizeBytes: number;
|
|
12
|
-
tokens: number;
|
|
13
|
-
}>;
|
|
14
|
-
export declare function scan(): Promise<ScanResult>;
|
|
15
|
-
export {};
|
|
1
|
+
export { scan } from './scanner/index.js';
|
|
2
|
+
export { SKILL_PROMPT_OVERHEAD_TOKENS } from './scanner/constants.js';
|
|
3
|
+
export { dedupeBySymlink } from './scanner/local-skills.js';
|
|
4
|
+
export { parseDisabledPlugins } from './scanner/disabled-plugins.js';
|
|
5
|
+
export { parseClaudeMdSections } from './scanner/claude-md.js';
|
package/dist/scanner.js
CHANGED
|
@@ -1,530 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
async function safeReadFile(p) {
|
|
10
|
-
try {
|
|
11
|
-
return await readFile(p, 'utf-8');
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
async function safeReaddir(p) {
|
|
18
|
-
try {
|
|
19
|
-
return await readdir(p);
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return [];
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
async function isDirectory(p) {
|
|
26
|
-
try {
|
|
27
|
-
return (await stat(p)).isDirectory();
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
async function isBrokenSymlink(p) {
|
|
34
|
-
try {
|
|
35
|
-
const lstats = await lstat(p);
|
|
36
|
-
if (!lstats.isSymbolicLink())
|
|
37
|
-
return false;
|
|
38
|
-
await realpath(p);
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
try {
|
|
43
|
-
return (await lstat(p)).isSymbolicLink();
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
// execFile (not exec) — never routes through a shell, so command arguments
|
|
51
|
-
// cannot be interpreted as shell metacharacters regardless of caller inputs.
|
|
52
|
-
async function runCommand(file, args) {
|
|
53
|
-
try {
|
|
54
|
-
const { execFile } = await import('node:child_process');
|
|
55
|
-
return new Promise((resolve) => {
|
|
56
|
-
execFile(file, args, { timeout: 10000 }, (_err, stdout) => {
|
|
57
|
-
resolve(stdout || '');
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return '';
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
async function getDirSize(dir) {
|
|
66
|
-
let total = 0;
|
|
67
|
-
const entries = await safeReaddir(dir);
|
|
68
|
-
for (const entry of entries) {
|
|
69
|
-
const p = join(dir, entry);
|
|
70
|
-
try {
|
|
71
|
-
const s = await stat(p);
|
|
72
|
-
if (s.isFile())
|
|
73
|
-
total += s.size;
|
|
74
|
-
else if (s.isDirectory())
|
|
75
|
-
total += await getDirSize(p);
|
|
76
|
-
}
|
|
77
|
-
catch { /* skip */ }
|
|
78
|
-
}
|
|
79
|
-
return total;
|
|
80
|
-
}
|
|
81
|
-
// Content cache: avoids re-reading files during classification.
|
|
82
|
-
// Reset on every scan() so repeat invocations (e.g. pre/post-cleanup) do
|
|
83
|
-
// not accumulate entries for paths that no longer exist.
|
|
84
|
-
const contentCache = new Map();
|
|
85
|
-
async function resolveRealPath(p) {
|
|
86
|
-
try {
|
|
87
|
-
return await realpath(p);
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return p;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
export function dedupeBySymlink(candidates) {
|
|
94
|
-
const seen = new Map();
|
|
95
|
-
for (const { skill, realMdPath } of candidates) {
|
|
96
|
-
const existing = seen.get(realMdPath);
|
|
97
|
-
if (!existing) {
|
|
98
|
-
seen.set(realMdPath, skill);
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
// Prefer top-level name (no slash) over nested duplicate
|
|
102
|
-
const existingIsNested = existing.name.includes('/');
|
|
103
|
-
const currentIsNested = skill.name.includes('/');
|
|
104
|
-
if (existingIsNested && !currentIsNested) {
|
|
105
|
-
seen.set(realMdPath, skill);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return Array.from(seen.values());
|
|
109
|
-
}
|
|
110
|
-
async function scanLocalSkills() {
|
|
111
|
-
const skillsDir = getSkillsDir();
|
|
112
|
-
const candidates = [];
|
|
113
|
-
const brokenSymlinks = [];
|
|
114
|
-
const entries = await safeReaddir(skillsDir);
|
|
115
|
-
const scanPromises = entries.map(async (entry) => {
|
|
116
|
-
const dirPath = join(skillsDir, entry);
|
|
117
|
-
if (!(await isDirectory(dirPath)))
|
|
118
|
-
return;
|
|
119
|
-
const skillMd = join(dirPath, 'SKILL.md');
|
|
120
|
-
if (await isBrokenSymlink(skillMd)) {
|
|
121
|
-
let target = 'unknown';
|
|
122
|
-
try {
|
|
123
|
-
target = await readlink(skillMd);
|
|
124
|
-
}
|
|
125
|
-
catch { /* */ }
|
|
126
|
-
brokenSymlinks.push({ name: entry, path: skillMd, target });
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
const content = await safeReadFile(skillMd);
|
|
130
|
-
if (content !== null) {
|
|
131
|
-
contentCache.set(skillMd, content);
|
|
132
|
-
const tokens = countTokensCached(content, skillMd);
|
|
133
|
-
const realMdPath = await resolveRealPath(skillMd);
|
|
134
|
-
candidates.push({
|
|
135
|
-
skill: {
|
|
136
|
-
name: entry,
|
|
137
|
-
path: dirPath,
|
|
138
|
-
sizeBytes: Buffer.byteLength(content),
|
|
139
|
-
tokens,
|
|
140
|
-
source: 'local',
|
|
141
|
-
},
|
|
142
|
-
realMdPath,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
// Nested skills (e.g., @internal-sys/commit-guide)
|
|
146
|
-
const subEntries = await safeReaddir(dirPath);
|
|
147
|
-
for (const sub of subEntries) {
|
|
148
|
-
const subDir = join(dirPath, sub);
|
|
149
|
-
if (!(await isDirectory(subDir)))
|
|
150
|
-
continue;
|
|
151
|
-
const subSkillMd = join(subDir, 'SKILL.md');
|
|
152
|
-
if (await isBrokenSymlink(subSkillMd)) {
|
|
153
|
-
let target = 'unknown';
|
|
154
|
-
try {
|
|
155
|
-
target = await readlink(subSkillMd);
|
|
156
|
-
}
|
|
157
|
-
catch { /* */ }
|
|
158
|
-
brokenSymlinks.push({ name: `${entry}/${sub}`, path: subSkillMd, target });
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
const subContent = await safeReadFile(subSkillMd);
|
|
162
|
-
if (subContent !== null) {
|
|
163
|
-
const name = `${entry}/${sub}`;
|
|
164
|
-
contentCache.set(subSkillMd, subContent);
|
|
165
|
-
const tokens = countTokensCached(subContent, subSkillMd);
|
|
166
|
-
const realMdPath = await resolveRealPath(subSkillMd);
|
|
167
|
-
candidates.push({
|
|
168
|
-
skill: {
|
|
169
|
-
name,
|
|
170
|
-
path: subDir,
|
|
171
|
-
sizeBytes: Buffer.byteLength(subContent),
|
|
172
|
-
tokens,
|
|
173
|
-
source: 'local',
|
|
174
|
-
},
|
|
175
|
-
realMdPath,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
await Promise.all(scanPromises);
|
|
181
|
-
const skills = dedupeBySymlink(candidates);
|
|
182
|
-
return { skills, brokenSymlinks };
|
|
183
|
-
}
|
|
184
|
-
async function scanPluginSkills() {
|
|
185
|
-
const skills = [];
|
|
186
|
-
const plugins = [];
|
|
187
|
-
const tempCaches = [];
|
|
188
|
-
const pluginsDir = getPluginsDir();
|
|
189
|
-
const pluginDirs = await safeReaddir(pluginsDir);
|
|
190
|
-
const scanPromises = pluginDirs.map(async (pluginName) => {
|
|
191
|
-
const pluginDir = join(pluginsDir, pluginName);
|
|
192
|
-
if (!(await isDirectory(pluginDir)))
|
|
193
|
-
return;
|
|
194
|
-
// Detect temp_local_* cache dirs (failed plugin installs)
|
|
195
|
-
if (pluginName.startsWith('temp_local_')) {
|
|
196
|
-
const size = await getDirSize(pluginDir);
|
|
197
|
-
tempCaches.push({ name: pluginName, path: pluginDir, sizeKB: Math.round(size / 1024) });
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const pluginSkillNames = [];
|
|
201
|
-
const walkDir = async (dir) => {
|
|
202
|
-
const entries = await safeReaddir(dir);
|
|
203
|
-
for (const entry of entries) {
|
|
204
|
-
const entryPath = join(dir, entry);
|
|
205
|
-
if (!(await isDirectory(entryPath)))
|
|
206
|
-
continue;
|
|
207
|
-
if (entry === 'skills') {
|
|
208
|
-
const skillDirs = await safeReaddir(entryPath);
|
|
209
|
-
for (const skillDir of skillDirs) {
|
|
210
|
-
const skillPath = join(entryPath, skillDir);
|
|
211
|
-
if (!(await isDirectory(skillPath)))
|
|
212
|
-
continue;
|
|
213
|
-
const skillMd = join(skillPath, 'SKILL.md');
|
|
214
|
-
const content = await safeReadFile(skillMd);
|
|
215
|
-
if (content !== null) {
|
|
216
|
-
pluginSkillNames.push(skillDir);
|
|
217
|
-
skills.push({
|
|
218
|
-
name: skillDir,
|
|
219
|
-
path: skillPath,
|
|
220
|
-
sizeBytes: Buffer.byteLength(content),
|
|
221
|
-
tokens: countTokensCached(content, skillMd),
|
|
222
|
-
source: 'plugin',
|
|
223
|
-
pluginName,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
await walkDir(entryPath);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
await walkDir(pluginDir);
|
|
234
|
-
if (pluginSkillNames.length > 0) {
|
|
235
|
-
plugins.push({
|
|
236
|
-
name: pluginName,
|
|
237
|
-
skillCount: pluginSkillNames.length,
|
|
238
|
-
skills: pluginSkillNames,
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
await Promise.all(scanPromises);
|
|
243
|
-
return { skills, plugins, tempCaches };
|
|
244
|
-
}
|
|
245
|
-
async function scanMemoryFiles() {
|
|
246
|
-
const memoryFiles = [];
|
|
247
|
-
const staleProjects = [];
|
|
248
|
-
const projectsDir = getProjectsDir();
|
|
249
|
-
const projectDirs = await safeReaddir(projectsDir);
|
|
250
|
-
const now = Date.now();
|
|
251
|
-
const scanPromises = projectDirs.map(async (project) => {
|
|
252
|
-
const memDir = join(projectsDir, project, 'memory');
|
|
253
|
-
const files = await safeReaddir(memDir);
|
|
254
|
-
const mdFiles = files.filter((f) => f.endsWith('.md'));
|
|
255
|
-
let newestMtime = 0;
|
|
256
|
-
let totalBytes = 0;
|
|
257
|
-
for (const file of mdFiles) {
|
|
258
|
-
const filePath = join(memDir, file);
|
|
259
|
-
const content = await safeReadFile(filePath);
|
|
260
|
-
if (content !== null) {
|
|
261
|
-
const sizeBytes = Buffer.byteLength(content);
|
|
262
|
-
memoryFiles.push({
|
|
263
|
-
project,
|
|
264
|
-
name: file,
|
|
265
|
-
path: filePath,
|
|
266
|
-
sizeBytes,
|
|
267
|
-
tokens: countTokensCached(content, filePath),
|
|
268
|
-
});
|
|
269
|
-
totalBytes += sizeBytes;
|
|
270
|
-
try {
|
|
271
|
-
const s = await stat(filePath);
|
|
272
|
-
if (s.mtimeMs > newestMtime)
|
|
273
|
-
newestMtime = s.mtimeMs;
|
|
274
|
-
}
|
|
275
|
-
catch { /* skip */ }
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
// Check for stale project (no files modified in 90+ days)
|
|
279
|
-
if (mdFiles.length > 0 && newestMtime > 0) {
|
|
280
|
-
const ageDays = Math.floor((now - newestMtime) / (1000 * 60 * 60 * 24));
|
|
281
|
-
if (ageDays > STALE_DAYS) {
|
|
282
|
-
staleProjects.push({ project, path: memDir, ageDays, fileCount: mdFiles.length, totalBytes });
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
await Promise.all(scanPromises);
|
|
287
|
-
return { memoryFiles, staleProjects };
|
|
288
|
-
}
|
|
289
|
-
export function parseDisabledPlugins(output) {
|
|
290
|
-
const disabled = new Set();
|
|
291
|
-
if (!output)
|
|
292
|
-
return disabled;
|
|
293
|
-
let currentName = null;
|
|
294
|
-
for (const line of output.split('\n')) {
|
|
295
|
-
const trimmed = line.trim();
|
|
296
|
-
if (trimmed.startsWith('\u276f')) {
|
|
297
|
-
const full = trimmed.split('\u276f')[1]?.trim() || '';
|
|
298
|
-
// Format: sub-plugin@marketplace — extract marketplace name for cache dir matching
|
|
299
|
-
currentName = full.includes('@') ? full.split('@')[1] : full;
|
|
300
|
-
}
|
|
301
|
-
else if (trimmed.toLowerCase().includes('disabled') && currentName) {
|
|
302
|
-
disabled.add(currentName);
|
|
303
|
-
currentName = null;
|
|
304
|
-
}
|
|
305
|
-
else if (trimmed.toLowerCase().includes('enabled')) {
|
|
306
|
-
currentName = null;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
return disabled;
|
|
310
|
-
}
|
|
311
|
-
async function getDisabledPlugins() {
|
|
312
|
-
return parseDisabledPlugins(await runCommand('claude', ['plugin', 'list']));
|
|
313
|
-
}
|
|
314
|
-
export function parseClaudeMdSections(content) {
|
|
315
|
-
const sections = [];
|
|
316
|
-
const lines = content.split('\n');
|
|
317
|
-
let currentName = null;
|
|
318
|
-
let currentContent = '';
|
|
319
|
-
for (const line of lines) {
|
|
320
|
-
if (line.startsWith('# ')) {
|
|
321
|
-
if (currentName !== null) {
|
|
322
|
-
sections.push({
|
|
323
|
-
name: currentName,
|
|
324
|
-
sizeBytes: Buffer.byteLength(currentContent),
|
|
325
|
-
tokens: countTokensCached(currentContent, `claude-md-section:${currentName}`),
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
else if (currentContent.trim()) {
|
|
329
|
-
sections.push({
|
|
330
|
-
name: '(preamble)',
|
|
331
|
-
sizeBytes: Buffer.byteLength(currentContent),
|
|
332
|
-
tokens: countTokensCached(currentContent, 'claude-md-section:preamble'),
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
currentName = line.slice(2).trim().slice(0, 60);
|
|
336
|
-
currentContent = line + '\n';
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
currentContent += line + '\n';
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
if (currentName !== null) {
|
|
343
|
-
sections.push({
|
|
344
|
-
name: currentName,
|
|
345
|
-
sizeBytes: Buffer.byteLength(currentContent),
|
|
346
|
-
tokens: countTokensCached(currentContent, `claude-md-section:${currentName}`),
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
else if (currentContent.trim()) {
|
|
350
|
-
sections.push({
|
|
351
|
-
name: '(preamble)',
|
|
352
|
-
sizeBytes: Buffer.byteLength(currentContent),
|
|
353
|
-
tokens: countTokensCached(currentContent, 'claude-md-section:preamble'),
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
return sections;
|
|
357
|
-
}
|
|
358
|
-
async function scanMcpServers() {
|
|
359
|
-
const content = await safeReadFile(join(getClaudeDir(), 'settings.json'));
|
|
360
|
-
if (!content)
|
|
361
|
-
return { count: 0, names: [] };
|
|
362
|
-
try {
|
|
363
|
-
const data = JSON.parse(content);
|
|
364
|
-
const servers = data.mcpServers || {};
|
|
365
|
-
const names = Object.keys(servers).sort();
|
|
366
|
-
return { count: names.length, names };
|
|
367
|
-
}
|
|
368
|
-
catch {
|
|
369
|
-
return { count: 0, names: [] };
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
function classifyIssues(localSkills, pluginSkills, brokenSymlinks, memoryFiles, tempCaches, staleProjects, disabledPlugins, plugins) {
|
|
373
|
-
const issues = [];
|
|
374
|
-
const pluginSkillNames = new Set(pluginSkills.map((s) => s.name));
|
|
375
|
-
// Tier 1: broken symlinks
|
|
376
|
-
for (const link of brokenSymlinks) {
|
|
377
|
-
issues.push({
|
|
378
|
-
type: 'broken_symlink',
|
|
379
|
-
tier: 1,
|
|
380
|
-
name: link.name,
|
|
381
|
-
detail: link.target,
|
|
382
|
-
tokens: 0,
|
|
383
|
-
path: link.path,
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
for (const skill of localSkills) {
|
|
387
|
-
const skillMdPath = join(skill.path, 'SKILL.md');
|
|
388
|
-
// Tier 1: template skills (use cached content instead of re-reading)
|
|
389
|
-
const content = contentCache.get(skillMdPath);
|
|
390
|
-
if (content && content.includes('Replace with description')) {
|
|
391
|
-
issues.push({
|
|
392
|
-
type: 'template',
|
|
393
|
-
tier: 1,
|
|
394
|
-
name: skill.name,
|
|
395
|
-
tokens: skill.tokens,
|
|
396
|
-
path: skill.path,
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
// Tier 2: duplicates (local + plugin) — check base name for nested skills
|
|
400
|
-
const baseName = skill.name.includes('/') ? skill.name.split('/').pop() : skill.name;
|
|
401
|
-
if (pluginSkillNames.has(baseName)) {
|
|
402
|
-
issues.push({
|
|
403
|
-
type: 'duplicate',
|
|
404
|
-
tier: 2,
|
|
405
|
-
name: skill.name,
|
|
406
|
-
detail: 'local+plugin',
|
|
407
|
-
tokens: skill.tokens,
|
|
408
|
-
path: skill.path,
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
// Tier 3: oversized skills
|
|
412
|
-
if (skill.sizeBytes > OVERSIZED_SKILL_BYTES) {
|
|
413
|
-
issues.push({
|
|
414
|
-
type: 'oversized_skill',
|
|
415
|
-
tier: 3,
|
|
416
|
-
name: skill.name,
|
|
417
|
-
detail: `${Math.round(skill.sizeBytes / 1024)}KB`,
|
|
418
|
-
tokens: skill.tokens,
|
|
419
|
-
path: skill.path,
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
// Tier 1: .skill/ duplicate directories
|
|
424
|
-
for (const skill of localSkills) {
|
|
425
|
-
const dotSkillDir = skill.path + '.skill';
|
|
426
|
-
if (localSkills.some((s) => s.path === dotSkillDir)) {
|
|
427
|
-
issues.push({
|
|
428
|
-
type: 'skill_dup',
|
|
429
|
-
tier: 1,
|
|
430
|
-
name: skill.name,
|
|
431
|
-
tokens: 0,
|
|
432
|
-
path: dotSkillDir,
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
// Tier 1: temp_local_* cache directories
|
|
437
|
-
for (const temp of tempCaches) {
|
|
438
|
-
issues.push({
|
|
439
|
-
type: 'temp_cache',
|
|
440
|
-
tier: 1,
|
|
441
|
-
name: temp.name,
|
|
442
|
-
detail: `${temp.sizeKB}KB`,
|
|
443
|
-
tokens: 0,
|
|
444
|
-
path: temp.path,
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
// Tier 2: oversized memory files
|
|
448
|
-
for (const mem of memoryFiles) {
|
|
449
|
-
if (mem.sizeBytes > OVERSIZED_MEMORY_BYTES) {
|
|
450
|
-
issues.push({
|
|
451
|
-
type: 'oversized_memory',
|
|
452
|
-
tier: 2,
|
|
453
|
-
name: `${mem.project}/${mem.name}`,
|
|
454
|
-
detail: `${Math.round(mem.sizeBytes / 1024)}KB`,
|
|
455
|
-
tokens: mem.tokens,
|
|
456
|
-
path: mem.path,
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
// Tier 2: stale project memory (90+ days inactive)
|
|
461
|
-
for (const stale of staleProjects) {
|
|
462
|
-
const memTokens = memoryFiles
|
|
463
|
-
.filter((m) => m.project === stale.project)
|
|
464
|
-
.reduce((sum, m) => sum + m.tokens, 0);
|
|
465
|
-
issues.push({
|
|
466
|
-
type: 'stale_project',
|
|
467
|
-
tier: 2,
|
|
468
|
-
name: stale.project,
|
|
469
|
-
detail: `${stale.ageDays}d, ${stale.fileCount} files, ${Math.round(stale.totalBytes / 1024)}KB`,
|
|
470
|
-
tokens: memTokens,
|
|
471
|
-
path: stale.path,
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
// Tier 2: disabled plugins still occupying cache
|
|
475
|
-
for (const plugin of plugins) {
|
|
476
|
-
if (disabledPlugins.has(plugin.name)) {
|
|
477
|
-
issues.push({
|
|
478
|
-
type: 'disabled_plugin',
|
|
479
|
-
tier: 2,
|
|
480
|
-
name: plugin.name,
|
|
481
|
-
detail: `${plugin.skillCount} skills`,
|
|
482
|
-
tokens: plugin.skillCount * SKILL_PROMPT_OVERHEAD_TOKENS,
|
|
483
|
-
path: join(getPluginsDir(), plugin.name),
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
// Sort by tier
|
|
488
|
-
issues.sort((a, b) => a.tier - b.tier);
|
|
489
|
-
return issues;
|
|
490
|
-
}
|
|
491
|
-
export async function scan() {
|
|
492
|
-
contentCache.clear();
|
|
493
|
-
const [{ skills: localSkills, brokenSymlinks }, { skills: pluginSkills, plugins, tempCaches }, { memoryFiles, staleProjects }, mcp, disabledPlugins,] = await Promise.all([
|
|
494
|
-
scanLocalSkills(),
|
|
495
|
-
scanPluginSkills(),
|
|
496
|
-
scanMemoryFiles(),
|
|
497
|
-
scanMcpServers(),
|
|
498
|
-
getDisabledPlugins(),
|
|
499
|
-
]);
|
|
500
|
-
// Annotate plugin status
|
|
501
|
-
for (const plugin of plugins) {
|
|
502
|
-
plugin.status = disabledPlugins.has(plugin.name) ? 'disabled' : 'enabled';
|
|
503
|
-
}
|
|
504
|
-
// CLAUDE.md
|
|
505
|
-
const claudeMdContent = await safeReadFile(join(getClaudeDir(), 'CLAUDE.md'));
|
|
506
|
-
const claudeMdBytes = claudeMdContent ? Buffer.byteLength(claudeMdContent) : 0;
|
|
507
|
-
const claudeMdTokens = claudeMdContent
|
|
508
|
-
? countTokensCached(claudeMdContent, join(getClaudeDir(), 'CLAUDE.md'))
|
|
509
|
-
: 0;
|
|
510
|
-
const claudeMdSections = claudeMdContent ? parseClaudeMdSections(claudeMdContent) : [];
|
|
511
|
-
const issues = classifyIssues(localSkills, pluginSkills, brokenSymlinks, memoryFiles, tempCaches, staleProjects, disabledPlugins, plugins);
|
|
512
|
-
// Estimate total tokens at startup
|
|
513
|
-
const skillListingTokens = (localSkills.length + pluginSkills.length) * SKILL_PROMPT_OVERHEAD_TOKENS;
|
|
514
|
-
const memoryTokens = memoryFiles.reduce((sum, m) => sum + m.tokens, 0);
|
|
515
|
-
const totalTokensBefore = skillListingTokens + claudeMdTokens + memoryTokens;
|
|
516
|
-
return {
|
|
517
|
-
localSkills,
|
|
518
|
-
pluginSkills,
|
|
519
|
-
plugins,
|
|
520
|
-
brokenSymlinks,
|
|
521
|
-
memoryFiles,
|
|
522
|
-
claudeMdBytes,
|
|
523
|
-
claudeMdTokens,
|
|
524
|
-
claudeMdSections,
|
|
525
|
-
mcpServers: mcp.count,
|
|
526
|
-
mcpServerNames: mcp.names,
|
|
527
|
-
issues,
|
|
528
|
-
totalTokensBefore,
|
|
529
|
-
};
|
|
530
|
-
}
|
|
1
|
+
// Public barrel — keeps the previously-exported surface stable while the
|
|
2
|
+
// implementation lives in src/scanner/*. External importers (cli.ts, tests,
|
|
3
|
+
// future consumers) do not need to know about the split.
|
|
4
|
+
export { scan } from './scanner/index.js';
|
|
5
|
+
export { SKILL_PROMPT_OVERHEAD_TOKENS } from './scanner/constants.js';
|
|
6
|
+
export { dedupeBySymlink } from './scanner/local-skills.js';
|
|
7
|
+
export { parseDisabledPlugins } from './scanner/disabled-plugins.js';
|
|
8
|
+
export { parseClaudeMdSections } from './scanner/claude-md.js';
|