@crafter/skillkit 0.1.1 → 0.1.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crafter/skillkit",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Local-first analytics for AI agent skills. Track usage, measure context budget, and prune what you don't use.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/bin.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { bold, cyan, dim, yellow } from "./tui/colors";
3
3
 
4
- const VERSION = "0.1.1";
4
+ const VERSION = "0.1.3";
5
5
 
6
6
  function printHelp(): void {
7
7
  console.log(`
@@ -4,10 +4,10 @@ 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";
8
- import { healthGauge } from "../tui/health";
7
+ import { bold, dim, green, red, yellow } from "../tui/colors";
9
8
 
10
- const CONTEXT_BUDGET = 16000;
9
+ const METADATA_BUDGET = 16000;
10
+ const BODY_LINE_LIMIT = 500;
11
11
 
12
12
  function check(label: string) {
13
13
  return ` ${green("✓")} ${label}`;
@@ -21,26 +21,67 @@ function info(label: string) {
21
21
  return ` ${dim("●")} ${label}`;
22
22
  }
23
23
 
24
- function _formatSize(bytes: number): string {
25
- if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
26
- return `${bytes} B`;
24
+ function parseFrontmatter(content: string): {
25
+ name: string;
26
+ description: string;
27
+ bodyLines: number;
28
+ } {
29
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
30
+ if (!match)
31
+ return { name: "", description: "", bodyLines: content.split("\n").length };
32
+
33
+ const yaml = match[1] ?? "";
34
+ const body = content.slice(match[0].length);
35
+ const bodyLines = body.trim() ? body.trim().split("\n").length : 0;
36
+
37
+ let name = "";
38
+ let description = "";
39
+
40
+ const nameMatch = yaml.match(/^name:\s*(.+)$/m);
41
+ if (nameMatch?.[1]) name = nameMatch[1].trim().replace(/^["']|["']$/g, "");
42
+
43
+ const descMatch = yaml.match(/^description:\s*(.+)$/m);
44
+ if (descMatch?.[1])
45
+ 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 totalContextChars = 0;
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
- try {
40
- const content = readFileSync(skillMdPath, "utf-8");
41
- totalContextChars += content.length;
42
- } catch {}
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 contextPct = Math.min(
110
+ const metadataPct = Math.min(
70
111
  100,
71
- Math.round((totalContextChars / CONTEXT_BUDGET) * 100),
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("SKILL-KIT HEALTH REPORT")}\n`);
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,66 @@ 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 - contextPct);
141
+ const metaKb = (totalMetadataChars / 1000).toFixed(1);
142
+ const budgetKb = (METADATA_BUDGET / 1000).toFixed(1);
143
+
144
+ const filled = Math.round(Math.min(metadataPct, 100) / 10);
145
+ const empty = 10 - filled;
146
+ const barFill = "█".repeat(filled);
147
+ const barEmpty = "░".repeat(empty);
148
+ const colorBar =
149
+ metadataPct >= 90
150
+ ? red(barFill)
151
+ : metadataPct >= 70
152
+ ? yellow(barFill)
153
+ : green(barFill);
154
+ const bar = `[${colorBar}${dim(barEmpty)}]`;
155
+
156
+ console.log(
157
+ ` ${bar} ${bold(`${metadataPct}%`)} metadata budget ${dim(`(${metaKb}K / ${budgetKb}K)`)}`,
158
+ );
115
159
  console.log(
116
- info(
117
- `Context budget: ${contextPct}% consumed (${usedContextKb}K / ${budgetKb}K chars)`,
118
- ),
160
+ ` ${dim("name + description of each skill, loaded at startup")}`,
119
161
  );
120
- console.log(` ${gaugeStr}`);
121
- if (unusedContextChars > 0) {
162
+
163
+ const bodyKb = (totalBodyChars / 1000).toFixed(1);
164
+ console.log(info(`Total skill content: ${bodyKb}K chars (loaded on-demand)`));
165
+
166
+ if (oversizedSkills.length > 0) {
167
+ console.log();
168
+ console.log(
169
+ warn(
170
+ `${oversizedSkills.length} skills exceed ${BODY_LINE_LIMIT}-line recommendation`,
171
+ ),
172
+ );
173
+ for (const s of oversizedSkills.slice(0, 5)) {
174
+ console.log(` ${dim(`${s.name}: ${s.lines} lines`)}`);
175
+ }
176
+ if (oversizedSkills.length > 5) {
177
+ console.log(` ${dim(`+${oversizedSkills.length - 5} more`)}`);
178
+ }
122
179
  console.log(
123
- ` ${dim(`${neverUsed.length} unused skills waste ~${(unusedContextChars / 1000).toFixed(1)}K chars`)}`,
180
+ ` ${dim("Split large SKILL.md into referenced files for progressive disclosure")}`,
124
181
  );
125
182
  }
126
183
 
184
+ if (longDescSkills.length > 0) {
185
+ console.log();
186
+ console.log(
187
+ warn(`${longDescSkills.length} skills have descriptions over 1024 chars`),
188
+ );
189
+ for (const s of longDescSkills.slice(0, 3)) {
190
+ console.log(` ${dim(`${s.name}: ${s.chars} chars`)}`);
191
+ }
192
+ }
193
+
127
194
  console.log();
128
195
  }