@crafter/skillkit 0.1.0 → 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 +32 -3
- package/src/bin.ts +14 -45
- package/src/commands/health.ts +111 -42
- package/src/commands/prune.ts +85 -0
- package/src/commands/scan.ts +117 -0
- package/src/commands/stats.ts +1 -1
- package/src/db/schema.ts +1 -1
- package/src/index.ts +2 -0
- package/src/commands/find.ts +0 -13
- package/src/commands/install.ts +0 -21
- package/src/commands/uninstall.ts +0 -16
- package/src/commands/update.ts +0 -11
- package/tsconfig.json +0 -9
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crafter/skillkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Local-first analytics for AI agent skills. Track usage, measure context budget, and prune what you don't use.",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"bin": {
|
|
6
|
-
"
|
|
7
|
+
"skillkit": "./src/bin.ts"
|
|
7
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
8
13
|
"exports": {
|
|
9
14
|
".": {
|
|
10
15
|
"bun": "./src/index.ts",
|
|
@@ -13,10 +18,34 @@
|
|
|
13
18
|
},
|
|
14
19
|
"scripts": {
|
|
15
20
|
"dev": "bun run src/bin.ts",
|
|
16
|
-
"build": "bun build src/bin.ts --compile --outfile dist/
|
|
21
|
+
"build": "bun build src/bin.ts --compile --outfile dist/skillkit",
|
|
17
22
|
"test": "bun test src/",
|
|
18
23
|
"type-check": "tsc --noEmit"
|
|
19
24
|
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"skillkit",
|
|
27
|
+
"skills",
|
|
28
|
+
"analytics",
|
|
29
|
+
"claude-code",
|
|
30
|
+
"cursor",
|
|
31
|
+
"ai-agent",
|
|
32
|
+
"context-window",
|
|
33
|
+
"cli"
|
|
34
|
+
],
|
|
35
|
+
"author": "Crafter Station <the.crafter.station@gmail.com>",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/crafter-station/skill-kit.git",
|
|
40
|
+
"directory": "packages/cli"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/crafter-station/skill-kit#readme",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/crafter-station/skill-kit/issues"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"bun": ">=1.0.0"
|
|
48
|
+
},
|
|
20
49
|
"devDependencies": {
|
|
21
50
|
"@types/bun": "^1.2.14",
|
|
22
51
|
"typescript": "^5.9.3"
|
package/src/bin.ts
CHANGED
|
@@ -1,60 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { bold, cyan, dim, yellow } from "./tui/colors";
|
|
3
3
|
|
|
4
|
-
const VERSION = "0.1.
|
|
4
|
+
const VERSION = "0.1.2";
|
|
5
5
|
|
|
6
6
|
function printHelp(): void {
|
|
7
7
|
console.log(`
|
|
8
|
-
${bold("
|
|
8
|
+
${bold("skillkit")} ${dim(`v${VERSION}`)} - Analytics for AI agent skills
|
|
9
9
|
|
|
10
10
|
${bold("USAGE")}
|
|
11
|
-
|
|
11
|
+
skillkit <command> [args]
|
|
12
12
|
|
|
13
|
-
${bold("
|
|
14
|
-
${cyan("
|
|
15
|
-
${cyan("uninstall")} Remove a skill ${dim("(skill-kit uninstall name)")}
|
|
16
|
-
${cyan("update")} Update all skills to latest
|
|
17
|
-
${cyan("find")} Search the skills.sh registry
|
|
18
|
-
|
|
19
|
-
${bold("ANALYTICS")} ${dim("(local-first)")}
|
|
13
|
+
${bold("COMMANDS")}
|
|
14
|
+
${cyan("scan")} Discover installed skills and index session data
|
|
20
15
|
${cyan("list")} List installed skills with size & context budget
|
|
21
16
|
${cyan("stats")} Usage analytics with sparklines (last 30 days)
|
|
22
17
|
${cyan("health")} Health check: unused skills, context budget, DB
|
|
23
|
-
${cyan("
|
|
24
|
-
|
|
25
|
-
${bold("OTHER")}
|
|
18
|
+
${cyan("prune")} Remove unused skills to reclaim context budget
|
|
26
19
|
${cyan("version")} Print version
|
|
27
20
|
${cyan("help")} Show this help message
|
|
21
|
+
|
|
22
|
+
${dim("Install skills via skills.sh: npx skills add <owner/repo>")}
|
|
28
23
|
`);
|
|
29
24
|
}
|
|
30
25
|
|
|
31
26
|
async function main(): Promise<void> {
|
|
32
27
|
const cmd = process.argv[2];
|
|
33
|
-
const args = process.argv.slice(3);
|
|
34
28
|
|
|
35
29
|
switch (cmd) {
|
|
36
|
-
case "
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
runInstall(args);
|
|
40
|
-
break;
|
|
41
|
-
}
|
|
42
|
-
case "uninstall":
|
|
43
|
-
case "remove": {
|
|
44
|
-
const { runUninstall } = await import("./commands/uninstall");
|
|
45
|
-
runUninstall(args);
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
case "update":
|
|
49
|
-
case "upgrade": {
|
|
50
|
-
const { runUpdate } = await import("./commands/update");
|
|
51
|
-
runUpdate();
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
case "find":
|
|
55
|
-
case "search": {
|
|
56
|
-
const { runFind } = await import("./commands/find");
|
|
57
|
-
runFind(args);
|
|
30
|
+
case "scan": {
|
|
31
|
+
const { runScan } = await import("./commands/scan");
|
|
32
|
+
await runScan();
|
|
58
33
|
break;
|
|
59
34
|
}
|
|
60
35
|
case "list":
|
|
@@ -73,15 +48,9 @@ async function main(): Promise<void> {
|
|
|
73
48
|
await runHealth();
|
|
74
49
|
break;
|
|
75
50
|
}
|
|
76
|
-
case "
|
|
77
|
-
const {
|
|
78
|
-
|
|
79
|
-
const db = getDb();
|
|
80
|
-
console.log("\n Scanning ~/.claude/projects/ for skill invocations...");
|
|
81
|
-
const count = await scanAllSessions(db);
|
|
82
|
-
console.log(
|
|
83
|
-
` ${count > 0 ? `Found ${count} new invocations.` : "No new invocations found."}\n`,
|
|
84
|
-
);
|
|
51
|
+
case "prune": {
|
|
52
|
+
const { runPrune } = await import("./commands/prune");
|
|
53
|
+
await runPrune();
|
|
85
54
|
break;
|
|
86
55
|
}
|
|
87
56
|
case "version":
|
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
|
|
32
|
-
const dbPath = join(homedir(), ".skill-kit", "analytics.db");
|
|
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
|
|
|
@@ -83,46 +122,76 @@ export async function runHealth(): Promise<void> {
|
|
|
83
122
|
);
|
|
84
123
|
} else if (dbExists) {
|
|
85
124
|
console.log(warn("Analytics DB exists but has no data"));
|
|
86
|
-
console.log(dim(" Run:
|
|
125
|
+
console.log(dim(" Run: skillkit scan"));
|
|
87
126
|
} else {
|
|
88
127
|
console.log(warn("Analytics DB not found"));
|
|
89
|
-
console.log(dim(" Run:
|
|
128
|
+
console.log(dim(" Run: skillkit scan"));
|
|
90
129
|
}
|
|
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
|
-
console.log(` ${dim("Run:
|
|
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
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getTopSkills } from "../db/queries";
|
|
5
|
+
import { getDb } from "../db/schema";
|
|
6
|
+
import { scanInstalledSkills } from "../scanner/skills";
|
|
7
|
+
import { bold, cyan, dim, red, yellow } from "../tui/colors";
|
|
8
|
+
|
|
9
|
+
export async function runPrune(): Promise<void> {
|
|
10
|
+
const dbPath = join(homedir(), ".skillkit", "analytics.db");
|
|
11
|
+
if (!existsSync(dbPath)) {
|
|
12
|
+
console.log(`\n ${yellow("No analytics data yet.")}`);
|
|
13
|
+
console.log(` ${dim("Run: skillkit scan")}\n`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const db = getDb();
|
|
18
|
+
const skills = scanInstalledSkills();
|
|
19
|
+
|
|
20
|
+
if (skills.length === 0) {
|
|
21
|
+
console.log(`\n ${dim("No skills installed.")}\n`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const topSkills = getTopSkills(db, 30);
|
|
26
|
+
const usedNames = new Set(topSkills.map((s) => s.skill_name));
|
|
27
|
+
const unused = skills.filter((s) => !usedNames.has(s.name));
|
|
28
|
+
|
|
29
|
+
if (unused.length === 0) {
|
|
30
|
+
console.log(
|
|
31
|
+
`\n ${dim("All")} ${bold(String(skills.length))} ${dim("skills were used in the last 30 days. Nothing to prune.")}\n`,
|
|
32
|
+
);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let totalWaste = 0;
|
|
37
|
+
const candidates: Array<{ name: string; path: string; chars: number }> = [];
|
|
38
|
+
|
|
39
|
+
for (const skill of unused) {
|
|
40
|
+
const skillMdPath = join(skill.path, "SKILL.md");
|
|
41
|
+
let chars = 0;
|
|
42
|
+
if (existsSync(skillMdPath)) {
|
|
43
|
+
try {
|
|
44
|
+
chars = readFileSync(skillMdPath, "utf-8").length;
|
|
45
|
+
} catch {}
|
|
46
|
+
}
|
|
47
|
+
totalWaste += chars;
|
|
48
|
+
candidates.push({ name: skill.name, path: skill.path, chars });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(
|
|
52
|
+
`\n ${bold("PRUNE")} ${dim("— unused skills in the last 30 days")}\n`,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
for (const c of candidates) {
|
|
56
|
+
const size = c.chars > 0 ? dim(` (${(c.chars / 1000).toFixed(1)}K)`) : "";
|
|
57
|
+
console.log(` ${red("×")} ${c.name}${size}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(
|
|
61
|
+
`\n ${bold(String(candidates.length))} skills ${dim("·")} ${bold(`${(totalWaste / 1000).toFixed(1)}K`)} ${dim("context reclaimable")}`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const args = process.argv.slice(3);
|
|
65
|
+
if (!args.includes("--yes") && !args.includes("-y")) {
|
|
66
|
+
console.log(
|
|
67
|
+
`\n ${dim("Run with")} ${cyan("--yes")} ${dim("to confirm deletion.")}\n`,
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let removed = 0;
|
|
73
|
+
for (const c of candidates) {
|
|
74
|
+
try {
|
|
75
|
+
rmSync(c.path, { recursive: true, force: true });
|
|
76
|
+
removed++;
|
|
77
|
+
} catch {
|
|
78
|
+
console.log(` ${yellow("!")} Failed to remove ${c.name}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(
|
|
83
|
+
`\n ${bold(`Removed ${removed} skills`)} ${dim("·")} ${bold(`${(totalWaste / 1000).toFixed(1)}K`)} ${dim("reclaimed")}\n`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { upsertInstalledSkill } from "../db/queries";
|
|
5
|
+
import { getDb } from "../db/schema";
|
|
6
|
+
import { scanAllSessions } from "../scanner/index";
|
|
7
|
+
import { scanInstalledSkills } from "../scanner/skills";
|
|
8
|
+
import { bold, cyan, dim } from "../tui/colors";
|
|
9
|
+
|
|
10
|
+
function detectSource(skillPath: string): "skills.sh" | "manual" {
|
|
11
|
+
const metaDir = join(skillPath, ".skills");
|
|
12
|
+
if (existsSync(metaDir)) return "skills.sh";
|
|
13
|
+
|
|
14
|
+
const skillMd = join(skillPath, "SKILL.md");
|
|
15
|
+
if (existsSync(skillMd)) {
|
|
16
|
+
try {
|
|
17
|
+
const entries = readdirSync(skillPath);
|
|
18
|
+
if (entries.includes(".git") || entries.includes(".gitmodules"))
|
|
19
|
+
return "skills.sh";
|
|
20
|
+
} catch {}
|
|
21
|
+
}
|
|
22
|
+
return "manual";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function countSessions(): number {
|
|
26
|
+
const projectsDir = join(homedir(), ".claude", "projects");
|
|
27
|
+
if (!existsSync(projectsDir)) return 0;
|
|
28
|
+
|
|
29
|
+
let count = 0;
|
|
30
|
+
const glob = new Bun.Glob("**/*.jsonl");
|
|
31
|
+
for (const _ of glob.scanSync({ cwd: projectsDir })) {
|
|
32
|
+
count++;
|
|
33
|
+
}
|
|
34
|
+
return count;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function runScan(): Promise<void> {
|
|
38
|
+
const db = getDb();
|
|
39
|
+
|
|
40
|
+
console.log(`\n ${dim("Scanning ~/.claude/skills/ ...")}`);
|
|
41
|
+
|
|
42
|
+
const skills = scanInstalledSkills();
|
|
43
|
+
|
|
44
|
+
let skillsShCount = 0;
|
|
45
|
+
let manualCount = 0;
|
|
46
|
+
|
|
47
|
+
for (const skill of skills) {
|
|
48
|
+
const source = detectSource(skill.path);
|
|
49
|
+
if (source === "skills.sh") skillsShCount++;
|
|
50
|
+
else manualCount++;
|
|
51
|
+
|
|
52
|
+
upsertInstalledSkill(
|
|
53
|
+
db,
|
|
54
|
+
skill.name,
|
|
55
|
+
skill.path,
|
|
56
|
+
source,
|
|
57
|
+
undefined,
|
|
58
|
+
skill.size,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (skills.length === 0) {
|
|
63
|
+
console.log(dim(" No skills found.\n"));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const parts: string[] = [];
|
|
68
|
+
if (skillsShCount > 0) parts.push(`${skillsShCount} via skills.sh`);
|
|
69
|
+
if (manualCount > 0) parts.push(`${manualCount} manual`);
|
|
70
|
+
|
|
71
|
+
console.log(
|
|
72
|
+
` ${bold(`Found ${skills.length} skills`)} ${dim(`(${parts.join(", ")})`)}`,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const localSkillsDir = join(process.cwd(), ".claude", "skills");
|
|
76
|
+
if (existsSync(localSkillsDir)) {
|
|
77
|
+
try {
|
|
78
|
+
const localEntries = readdirSync(localSkillsDir).filter((e) => {
|
|
79
|
+
try {
|
|
80
|
+
return statSync(join(localSkillsDir, e)).isDirectory();
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
if (localEntries.length > 0) {
|
|
86
|
+
console.log(
|
|
87
|
+
dim(
|
|
88
|
+
` + ${localEntries.length} project-local skills in .claude/skills/`,
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(dim(" Scanning sessions..."));
|
|
96
|
+
|
|
97
|
+
const sessionCount = countSessions();
|
|
98
|
+
const newInvocations = await scanAllSessions(db);
|
|
99
|
+
const totalRow = db
|
|
100
|
+
.query<{ count: number }, []>(
|
|
101
|
+
"SELECT COUNT(*) as count FROM skill_invocations",
|
|
102
|
+
)
|
|
103
|
+
.get();
|
|
104
|
+
const totalInvocations = totalRow?.count ?? 0;
|
|
105
|
+
|
|
106
|
+
console.log(
|
|
107
|
+
` ${dim("Indexed")} ${bold(String(sessionCount))} ${dim("sessions")} ${cyan("·")} ${bold(totalInvocations.toLocaleString())} ${dim("invocations")}`,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (newInvocations > 0) {
|
|
111
|
+
console.log(dim(` (${newInvocations} new)`));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(
|
|
115
|
+
`\n ${dim("Ready. Run")} ${cyan("skillkit stats")} ${dim("to see usage.")}\n`,
|
|
116
|
+
);
|
|
117
|
+
}
|
package/src/commands/stats.ts
CHANGED
|
@@ -35,7 +35,7 @@ export async function runStats(): Promise<void> {
|
|
|
35
35
|
|
|
36
36
|
if (stats.total === 0) {
|
|
37
37
|
console.log(`\n ${yellow("No analytics data yet.")}`);
|
|
38
|
-
console.log(` ${dim("Run:
|
|
38
|
+
console.log(` ${dim("Run: skillkit scan")}\n`);
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
|
package/src/db/schema.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdirSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
-
const DB_DIR = join(homedir(), ".
|
|
6
|
+
const DB_DIR = join(homedir(), ".skillkit");
|
|
7
7
|
const DB_PATH = join(DB_DIR, "analytics.db");
|
|
8
8
|
|
|
9
9
|
export function getDb(): Database {
|
package/src/index.ts
CHANGED
package/src/commands/find.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import { dim } from "../tui/colors";
|
|
3
|
-
|
|
4
|
-
export function runFind(args: string[]): void {
|
|
5
|
-
const query = args.join(" ");
|
|
6
|
-
console.log(`\n ${dim("Searching skills.sh registry...")}\n`);
|
|
7
|
-
try {
|
|
8
|
-
const cmd = query ? `npx -y skills find ${query}` : "npx -y skills find";
|
|
9
|
-
execSync(cmd, { stdio: "inherit" });
|
|
10
|
-
} catch {
|
|
11
|
-
process.exit(1);
|
|
12
|
-
}
|
|
13
|
-
}
|
package/src/commands/install.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import { bold, dim, yellow } from "../tui/colors";
|
|
3
|
-
|
|
4
|
-
export function runInstall(args: string[]): void {
|
|
5
|
-
const source = args.join(" ");
|
|
6
|
-
if (!source) {
|
|
7
|
-
console.error(`\n ${yellow("Usage:")} skill-kit install <owner/repo>\n`);
|
|
8
|
-
console.log(` ${dim("Examples:")}`);
|
|
9
|
-
console.log(` skill-kit install vercel-labs/agent-skills`);
|
|
10
|
-
console.log(` skill-kit install crafter-station/skill-kit\n`);
|
|
11
|
-
process.exit(1);
|
|
12
|
-
}
|
|
13
|
-
console.log(
|
|
14
|
-
`\n ${dim("Installing via")} ${bold("skills.sh")}${dim("...")}\n`,
|
|
15
|
-
);
|
|
16
|
-
try {
|
|
17
|
-
execSync(`npx -y skills add ${source}`, { stdio: "inherit" });
|
|
18
|
-
} catch {
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import { dim, yellow } from "../tui/colors";
|
|
3
|
-
|
|
4
|
-
export function runUninstall(args: string[]): void {
|
|
5
|
-
const name = args.join(" ");
|
|
6
|
-
if (!name) {
|
|
7
|
-
console.error(`\n ${yellow("Usage:")} skill-kit uninstall <skill-name>\n`);
|
|
8
|
-
process.exit(1);
|
|
9
|
-
}
|
|
10
|
-
console.log(`\n ${dim("Removing via skills.sh...")}\n`);
|
|
11
|
-
try {
|
|
12
|
-
execSync(`npx -y skills remove ${name}`, { stdio: "inherit" });
|
|
13
|
-
} catch {
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
|
-
}
|
package/src/commands/update.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import { dim } from "../tui/colors";
|
|
3
|
-
|
|
4
|
-
export function runUpdate(): void {
|
|
5
|
-
console.log(`\n ${dim("Checking for updates via skills.sh...")}\n`);
|
|
6
|
-
try {
|
|
7
|
-
execSync("npx -y skills update", { stdio: "inherit" });
|
|
8
|
-
} catch {
|
|
9
|
-
process.exit(1);
|
|
10
|
-
}
|
|
11
|
-
}
|