@crafter/skillkit 0.1.1 → 0.1.2
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 +1 -1
- package/src/bin.ts +1 -1
- package/src/commands/health.ts +107 -38
package/package.json
CHANGED
package/src/bin.ts
CHANGED
package/src/commands/health.ts
CHANGED
|
@@ -4,10 +4,11 @@ import { join } from "node:path";
|
|
|
4
4
|
import { getTopSkills } from "../db/queries";
|
|
5
5
|
import { getDb } from "../db/schema";
|
|
6
6
|
import { scanInstalledSkills } from "../scanner/skills";
|
|
7
|
-
import { bold, dim, green, yellow } from "../tui/colors";
|
|
7
|
+
import { bold, dim, green, red, yellow } from "../tui/colors";
|
|
8
8
|
import { healthGauge } from "../tui/health";
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const METADATA_BUDGET = 16000;
|
|
11
|
+
const BODY_LINE_LIMIT = 500;
|
|
11
12
|
|
|
12
13
|
function check(label: string) {
|
|
13
14
|
return ` ${green("✓")} ${label}`;
|
|
@@ -21,26 +22,66 @@ function info(label: string) {
|
|
|
21
22
|
return ` ${dim("●")} ${label}`;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
function parseFrontmatter(content: string): {
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
bodyLines: number;
|
|
29
|
+
} {
|
|
30
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
31
|
+
if (!match)
|
|
32
|
+
return { name: "", description: "", bodyLines: content.split("\n").length };
|
|
33
|
+
|
|
34
|
+
const yaml = match[1];
|
|
35
|
+
const body = content.slice(match[0].length);
|
|
36
|
+
const bodyLines = body.trim() ? body.trim().split("\n").length : 0;
|
|
37
|
+
|
|
38
|
+
let name = "";
|
|
39
|
+
let description = "";
|
|
40
|
+
|
|
41
|
+
const nameMatch = yaml.match(/^name:\s*(.+)$/m);
|
|
42
|
+
if (nameMatch) name = nameMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
43
|
+
|
|
44
|
+
const descMatch = yaml.match(/^description:\s*(.+)$/m);
|
|
45
|
+
if (descMatch) description = descMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
46
|
+
|
|
47
|
+
return { name, description, bodyLines };
|
|
27
48
|
}
|
|
28
49
|
|
|
29
50
|
export async function runHealth(): Promise<void> {
|
|
30
51
|
const skills = scanInstalledSkills();
|
|
31
|
-
const _skillsDir = join(homedir(), ".claude", "skills");
|
|
32
52
|
const dbPath = join(homedir(), ".skillkit", "analytics.db");
|
|
33
53
|
const dbExists = existsSync(dbPath);
|
|
34
54
|
|
|
35
|
-
let
|
|
55
|
+
let totalMetadataChars = 0;
|
|
56
|
+
let totalBodyChars = 0;
|
|
57
|
+
const oversizedSkills: Array<{ name: string; lines: number }> = [];
|
|
58
|
+
const longDescSkills: Array<{ name: string; chars: number }> = [];
|
|
59
|
+
|
|
36
60
|
for (const skill of skills) {
|
|
37
61
|
const skillMdPath = join(skill.path, "SKILL.md");
|
|
38
|
-
if (existsSync(skillMdPath))
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
62
|
+
if (!existsSync(skillMdPath)) continue;
|
|
63
|
+
try {
|
|
64
|
+
const content = readFileSync(skillMdPath, "utf-8");
|
|
65
|
+
const fm = parseFrontmatter(content);
|
|
66
|
+
|
|
67
|
+
const metadataSize =
|
|
68
|
+
(fm.name || skill.name).length + fm.description.length;
|
|
69
|
+
totalMetadataChars += metadataSize;
|
|
70
|
+
totalBodyChars += content.length;
|
|
71
|
+
|
|
72
|
+
if (fm.bodyLines > BODY_LINE_LIMIT) {
|
|
73
|
+
oversizedSkills.push({
|
|
74
|
+
name: fm.name || skill.name,
|
|
75
|
+
lines: fm.bodyLines,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (fm.description.length > 1024) {
|
|
79
|
+
longDescSkills.push({
|
|
80
|
+
name: fm.name || skill.name,
|
|
81
|
+
chars: fm.description.length,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
44
85
|
}
|
|
45
86
|
|
|
46
87
|
let eventCount = 0;
|
|
@@ -66,14 +107,12 @@ export async function runHealth(): Promise<void> {
|
|
|
66
107
|
}
|
|
67
108
|
}
|
|
68
109
|
|
|
69
|
-
const
|
|
110
|
+
const metadataPct = Math.min(
|
|
70
111
|
100,
|
|
71
|
-
Math.round((
|
|
112
|
+
Math.round((totalMetadataChars / METADATA_BUDGET) * 100),
|
|
72
113
|
);
|
|
73
|
-
const usedContextKb = (totalContextChars / 1000).toFixed(1);
|
|
74
|
-
const budgetKb = (CONTEXT_BUDGET / 1000).toFixed(1);
|
|
75
114
|
|
|
76
|
-
console.log(`\n ${bold("
|
|
115
|
+
console.log(`\n ${bold("SKILLKIT HEALTH REPORT")}\n`);
|
|
77
116
|
|
|
78
117
|
console.log(check(`${skills.length} skills installed`));
|
|
79
118
|
|
|
@@ -91,38 +130,68 @@ export async function runHealth(): Promise<void> {
|
|
|
91
130
|
|
|
92
131
|
if (neverUsed.length > 0) {
|
|
93
132
|
console.log();
|
|
94
|
-
console.log(warn(`${neverUsed.length} skills never used`));
|
|
133
|
+
console.log(warn(`${neverUsed.length} skills never used in 30d`));
|
|
95
134
|
const preview = neverUsed.slice(0, 5).join(", ");
|
|
96
135
|
const more = neverUsed.length > 5 ? ` +${neverUsed.length - 5} more` : "";
|
|
97
136
|
console.log(` ${dim(preview + more)}`);
|
|
98
137
|
console.log(` ${dim("Run: skillkit prune")}`);
|
|
99
138
|
}
|
|
100
139
|
|
|
101
|
-
const unusedContextChars = neverUsed.reduce((acc, name) => {
|
|
102
|
-
const skill = skills.find((s) => s.name === name);
|
|
103
|
-
if (!skill) return acc;
|
|
104
|
-
const skillMdPath = join(skill.path, "SKILL.md");
|
|
105
|
-
if (!existsSync(skillMdPath)) return acc;
|
|
106
|
-
try {
|
|
107
|
-
return acc + readFileSync(skillMdPath, "utf-8").length;
|
|
108
|
-
} catch {
|
|
109
|
-
return acc;
|
|
110
|
-
}
|
|
111
|
-
}, 0);
|
|
112
|
-
|
|
113
140
|
console.log();
|
|
114
|
-
const gaugeStr = healthGauge(100 -
|
|
141
|
+
const gaugeStr = healthGauge(100 - metadataPct);
|
|
142
|
+
const metaKb = (totalMetadataChars / 1000).toFixed(1);
|
|
143
|
+
const budgetKb = (METADATA_BUDGET / 1000).toFixed(1);
|
|
144
|
+
|
|
145
|
+
if (metadataPct >= 90) {
|
|
146
|
+
console.log(
|
|
147
|
+
` ${red("●")} Metadata budget: ${metadataPct}% (${metaKb}K / ${budgetKb}K chars)`,
|
|
148
|
+
);
|
|
149
|
+
} else if (metadataPct >= 70) {
|
|
150
|
+
console.log(
|
|
151
|
+
` ${yellow("●")} Metadata budget: ${metadataPct}% (${metaKb}K / ${budgetKb}K chars)`,
|
|
152
|
+
);
|
|
153
|
+
} else {
|
|
154
|
+
console.log(
|
|
155
|
+
info(
|
|
156
|
+
`Metadata budget: ${metadataPct}% (${metaKb}K / ${budgetKb}K chars)`,
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
console.log(` ${gaugeStr}`);
|
|
115
161
|
console.log(
|
|
116
|
-
|
|
117
|
-
`Context budget: ${contextPct}% consumed (${usedContextKb}K / ${budgetKb}K chars)`,
|
|
118
|
-
),
|
|
162
|
+
` ${dim(`Names + descriptions loaded at startup (2% of context window)`)}`,
|
|
119
163
|
);
|
|
120
|
-
|
|
121
|
-
|
|
164
|
+
|
|
165
|
+
const bodyKb = (totalBodyChars / 1000).toFixed(1);
|
|
166
|
+
console.log(info(`Total skill content: ${bodyKb}K chars (loaded on-demand)`));
|
|
167
|
+
|
|
168
|
+
if (oversizedSkills.length > 0) {
|
|
169
|
+
console.log();
|
|
170
|
+
console.log(
|
|
171
|
+
warn(
|
|
172
|
+
`${oversizedSkills.length} skills exceed ${BODY_LINE_LIMIT}-line recommendation`,
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
for (const s of oversizedSkills.slice(0, 5)) {
|
|
176
|
+
console.log(` ${dim(`${s.name}: ${s.lines} lines`)}`);
|
|
177
|
+
}
|
|
178
|
+
if (oversizedSkills.length > 5) {
|
|
179
|
+
console.log(` ${dim(`+${oversizedSkills.length - 5} more`)}`);
|
|
180
|
+
}
|
|
122
181
|
console.log(
|
|
123
|
-
` ${dim(
|
|
182
|
+
` ${dim("Split large SKILL.md into referenced files for progressive disclosure")}`,
|
|
124
183
|
);
|
|
125
184
|
}
|
|
126
185
|
|
|
186
|
+
if (longDescSkills.length > 0) {
|
|
187
|
+
console.log();
|
|
188
|
+
console.log(
|
|
189
|
+
warn(`${longDescSkills.length} skills have descriptions over 1024 chars`),
|
|
190
|
+
);
|
|
191
|
+
for (const s of longDescSkills.slice(0, 3)) {
|
|
192
|
+
console.log(` ${dim(`${s.name}: ${s.chars} chars`)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
127
196
|
console.log();
|
|
128
197
|
}
|