claude-slim 2.2.3 → 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 +53 -19
- package/dist/cleaner.js +2 -1
- package/dist/cli.js +12 -6
- 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 +23 -0
- package/dist/scanner/detectors.js +215 -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 +5 -0
- package/dist/scanner/index.js +62 -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/sessions.d.ts +8 -0
- package/dist/scanner/sessions.js +150 -0
- package/dist/scanner.d.ts +5 -15
- package/dist/scanner.js +8 -530
- package/dist/types.d.ts +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { countTokensCached } from '../tokenizer.js';
|
|
3
|
+
import { getClaudeDir } from '../paths.js';
|
|
4
|
+
import { safeReadFile } from './fs-walk.js';
|
|
5
|
+
import { scanLocalSkills } from './local-skills.js';
|
|
6
|
+
import { scanPluginSkills } from './plugin-skills.js';
|
|
7
|
+
import { scanMemoryFiles } from './memory.js';
|
|
8
|
+
import { scanMcpServers } from './mcp.js';
|
|
9
|
+
import { parseClaudeMdSections } from './claude-md.js';
|
|
10
|
+
import { getDisabledPlugins } from './disabled-plugins.js';
|
|
11
|
+
import { scanSessionUsage } from './sessions.js';
|
|
12
|
+
import { classifyIssues } from './detectors.js';
|
|
13
|
+
import { SKILL_PROMPT_OVERHEAD_TOKENS } from './constants.js';
|
|
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([
|
|
18
|
+
scanLocalSkills(),
|
|
19
|
+
scanPluginSkills(),
|
|
20
|
+
scanMemoryFiles(),
|
|
21
|
+
scanMcpServers(),
|
|
22
|
+
getDisabledPlugins(),
|
|
23
|
+
scanSessionUsage(lookbackDays),
|
|
24
|
+
]);
|
|
25
|
+
// Annotate plugin status
|
|
26
|
+
for (const plugin of plugins) {
|
|
27
|
+
plugin.status = disabledPlugins.has(plugin.name) ? 'disabled' : 'enabled';
|
|
28
|
+
}
|
|
29
|
+
// CLAUDE.md
|
|
30
|
+
const claudeMdContent = await safeReadFile(join(getClaudeDir(), 'CLAUDE.md'));
|
|
31
|
+
const claudeMdBytes = claudeMdContent ? Buffer.byteLength(claudeMdContent) : 0;
|
|
32
|
+
const claudeMdTokens = claudeMdContent
|
|
33
|
+
? countTokensCached(claudeMdContent, join(getClaudeDir(), 'CLAUDE.md'))
|
|
34
|
+
: 0;
|
|
35
|
+
const claudeMdSections = claudeMdContent ? parseClaudeMdSections(claudeMdContent) : [];
|
|
36
|
+
const issues = classifyIssues({
|
|
37
|
+
localSkills, pluginSkills, brokenSymlinks, memoryFiles,
|
|
38
|
+
tempCaches, staleProjects, disabledPlugins, plugins,
|
|
39
|
+
contents,
|
|
40
|
+
recentSkillInvocations: sessionUsage.invokedSkills,
|
|
41
|
+
sessionDataAvailable: sessionUsage.dataAvailable,
|
|
42
|
+
lookbackDays,
|
|
43
|
+
});
|
|
44
|
+
// Estimate total tokens at startup
|
|
45
|
+
const skillListingTokens = (localSkills.length + pluginSkills.length) * SKILL_PROMPT_OVERHEAD_TOKENS;
|
|
46
|
+
const memoryTokens = memoryFiles.reduce((sum, m) => sum + m.tokens, 0);
|
|
47
|
+
const totalTokensBefore = skillListingTokens + claudeMdTokens + memoryTokens;
|
|
48
|
+
return {
|
|
49
|
+
localSkills,
|
|
50
|
+
pluginSkills,
|
|
51
|
+
plugins,
|
|
52
|
+
brokenSymlinks,
|
|
53
|
+
memoryFiles,
|
|
54
|
+
claudeMdBytes,
|
|
55
|
+
claudeMdTokens,
|
|
56
|
+
claudeMdSections,
|
|
57
|
+
mcpServers: mcp.count,
|
|
58
|
+
mcpServerNames: mcp.names,
|
|
59
|
+
issues,
|
|
60
|
+
totalTokensBefore,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SkillInfo, BrokenSymlink } from '../types.js';
|
|
2
|
+
export interface SkillCandidate {
|
|
3
|
+
skill: SkillInfo;
|
|
4
|
+
realMdPath: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function dedupeBySymlink(candidates: SkillCandidate[]): SkillInfo[];
|
|
7
|
+
export interface LocalSkillsResult {
|
|
8
|
+
skills: SkillInfo[];
|
|
9
|
+
brokenSymlinks: BrokenSymlink[];
|
|
10
|
+
contents: Map<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export declare function scanLocalSkills(): Promise<LocalSkillsResult>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readlink } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { countTokensCached } from '../tokenizer.js';
|
|
4
|
+
import { getSkillsDir } from '../paths.js';
|
|
5
|
+
import { safeReadFile, safeReaddir, isDirectory, isBrokenSymlink, resolveRealPath, } from './fs-walk.js';
|
|
6
|
+
export function dedupeBySymlink(candidates) {
|
|
7
|
+
const seen = new Map();
|
|
8
|
+
for (const { skill, realMdPath } of candidates) {
|
|
9
|
+
const existing = seen.get(realMdPath);
|
|
10
|
+
if (!existing) {
|
|
11
|
+
seen.set(realMdPath, skill);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
// Prefer top-level name (no slash) over nested duplicate
|
|
15
|
+
const existingIsNested = existing.name.includes('/');
|
|
16
|
+
const currentIsNested = skill.name.includes('/');
|
|
17
|
+
if (existingIsNested && !currentIsNested) {
|
|
18
|
+
seen.set(realMdPath, skill);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return Array.from(seen.values());
|
|
22
|
+
}
|
|
23
|
+
export async function scanLocalSkills() {
|
|
24
|
+
const skillsDir = getSkillsDir();
|
|
25
|
+
const candidates = [];
|
|
26
|
+
const brokenSymlinks = [];
|
|
27
|
+
const contents = new Map();
|
|
28
|
+
const entries = await safeReaddir(skillsDir);
|
|
29
|
+
const scanPromises = entries.map(async (entry) => {
|
|
30
|
+
const dirPath = join(skillsDir, entry);
|
|
31
|
+
if (!(await isDirectory(dirPath)))
|
|
32
|
+
return;
|
|
33
|
+
const skillMd = join(dirPath, 'SKILL.md');
|
|
34
|
+
if (await isBrokenSymlink(skillMd)) {
|
|
35
|
+
let target = 'unknown';
|
|
36
|
+
try {
|
|
37
|
+
target = await readlink(skillMd);
|
|
38
|
+
}
|
|
39
|
+
catch { /* */ }
|
|
40
|
+
brokenSymlinks.push({ name: entry, path: skillMd, target });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const content = await safeReadFile(skillMd);
|
|
44
|
+
if (content !== null) {
|
|
45
|
+
contents.set(skillMd, content);
|
|
46
|
+
const tokens = countTokensCached(content, skillMd);
|
|
47
|
+
const realMdPath = await resolveRealPath(skillMd);
|
|
48
|
+
candidates.push({
|
|
49
|
+
skill: {
|
|
50
|
+
name: entry,
|
|
51
|
+
path: dirPath,
|
|
52
|
+
sizeBytes: Buffer.byteLength(content),
|
|
53
|
+
tokens,
|
|
54
|
+
source: 'local',
|
|
55
|
+
},
|
|
56
|
+
realMdPath,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Nested skills (e.g., @internal-sys/commit-guide)
|
|
60
|
+
const subEntries = await safeReaddir(dirPath);
|
|
61
|
+
for (const sub of subEntries) {
|
|
62
|
+
const subDir = join(dirPath, sub);
|
|
63
|
+
if (!(await isDirectory(subDir)))
|
|
64
|
+
continue;
|
|
65
|
+
const subSkillMd = join(subDir, 'SKILL.md');
|
|
66
|
+
if (await isBrokenSymlink(subSkillMd)) {
|
|
67
|
+
let target = 'unknown';
|
|
68
|
+
try {
|
|
69
|
+
target = await readlink(subSkillMd);
|
|
70
|
+
}
|
|
71
|
+
catch { /* */ }
|
|
72
|
+
brokenSymlinks.push({ name: `${entry}/${sub}`, path: subSkillMd, target });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const subContent = await safeReadFile(subSkillMd);
|
|
76
|
+
if (subContent !== null) {
|
|
77
|
+
const name = `${entry}/${sub}`;
|
|
78
|
+
contents.set(subSkillMd, subContent);
|
|
79
|
+
const tokens = countTokensCached(subContent, subSkillMd);
|
|
80
|
+
const realMdPath = await resolveRealPath(subSkillMd);
|
|
81
|
+
candidates.push({
|
|
82
|
+
skill: {
|
|
83
|
+
name,
|
|
84
|
+
path: subDir,
|
|
85
|
+
sizeBytes: Buffer.byteLength(subContent),
|
|
86
|
+
tokens,
|
|
87
|
+
source: 'local',
|
|
88
|
+
},
|
|
89
|
+
realMdPath,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
await Promise.all(scanPromises);
|
|
95
|
+
const skills = dedupeBySymlink(candidates);
|
|
96
|
+
return { skills, brokenSymlinks, contents };
|
|
97
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { getClaudeDir } from '../paths.js';
|
|
3
|
+
import { safeReadFile } from './fs-walk.js';
|
|
4
|
+
export async function scanMcpServers() {
|
|
5
|
+
const content = await safeReadFile(join(getClaudeDir(), 'settings.json'));
|
|
6
|
+
if (!content)
|
|
7
|
+
return { count: 0, names: [] };
|
|
8
|
+
try {
|
|
9
|
+
const data = JSON.parse(content);
|
|
10
|
+
const servers = data.mcpServers || {};
|
|
11
|
+
const names = Object.keys(servers).sort();
|
|
12
|
+
return { count: names.length, names };
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return { count: 0, names: [] };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MemoryFile } from '../types.js';
|
|
2
|
+
export interface StaleProject {
|
|
3
|
+
project: string;
|
|
4
|
+
path: string;
|
|
5
|
+
ageDays: number;
|
|
6
|
+
fileCount: number;
|
|
7
|
+
totalBytes: number;
|
|
8
|
+
}
|
|
9
|
+
export interface MemoryScanResult {
|
|
10
|
+
memoryFiles: MemoryFile[];
|
|
11
|
+
staleProjects: StaleProject[];
|
|
12
|
+
}
|
|
13
|
+
export declare function scanMemoryFiles(): Promise<MemoryScanResult>;
|
|
@@ -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
|
+
}
|
|
@@ -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/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';
|