@crafter/skillkit 0.0.1 → 0.1.1

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,10 +1,15 @@
1
1
  {
2
2
  "name": "@crafter/skillkit",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
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
- "skill-kit": "./src/bin.ts"
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/skill-kit",
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,22 +1,25 @@
1
1
  #!/usr/bin/env bun
2
2
  import { bold, cyan, dim, yellow } from "./tui/colors";
3
3
 
4
- const VERSION = "0.0.1";
4
+ const VERSION = "0.1.1";
5
5
 
6
6
  function printHelp(): void {
7
7
  console.log(`
8
- ${bold("skill-kit")} ${dim(`v${VERSION}`)} - Claude skill analytics & management
8
+ ${bold("skillkit")} ${dim(`v${VERSION}`)} - Analytics for AI agent skills
9
9
 
10
10
  ${bold("USAGE")}
11
- skill-kit <command>
11
+ skillkit <command> [args]
12
12
 
13
13
  ${bold("COMMANDS")}
14
- ${cyan("list")} List all installed skills
15
- ${cyan("stats")} Show usage analytics (last 30 days)
16
- ${cyan("health")} Run a health check on your skill setup
17
- ${cyan("analyze")} Scan sessions and populate analytics DB
18
- ${cyan("version")} Print version
19
- ${cyan("help")} Show this help message
14
+ ${cyan("scan")} Discover installed skills and index session data
15
+ ${cyan("list")} List installed skills with size & context budget
16
+ ${cyan("stats")} Usage analytics with sparklines (last 30 days)
17
+ ${cyan("health")} Health check: unused skills, context budget, DB
18
+ ${cyan("prune")} Remove unused skills to reclaim context budget
19
+ ${cyan("version")} Print version
20
+ ${cyan("help")} Show this help message
21
+
22
+ ${dim("Install skills via skills.sh: npx skills add <owner/repo>")}
20
23
  `);
21
24
  }
22
25
 
@@ -24,7 +27,13 @@ async function main(): Promise<void> {
24
27
  const cmd = process.argv[2];
25
28
 
26
29
  switch (cmd) {
27
- case "list": {
30
+ case "scan": {
31
+ const { runScan } = await import("./commands/scan");
32
+ await runScan();
33
+ break;
34
+ }
35
+ case "list":
36
+ case "ls": {
28
37
  const { runList } = await import("./commands/list");
29
38
  runList();
30
39
  break;
@@ -39,15 +48,9 @@ async function main(): Promise<void> {
39
48
  await runHealth();
40
49
  break;
41
50
  }
42
- case "analyze": {
43
- const { getDb } = await import("./db/schema");
44
- const { scanAllSessions } = await import("./scanner/index");
45
- const db = getDb();
46
- console.log("\n Scanning ~/.claude/projects/ for skill invocations...");
47
- const count = await scanAllSessions(db);
48
- console.log(
49
- ` ${count > 0 ? `Found ${count} new invocations.` : "No new invocations found."}\n`,
50
- );
51
+ case "prune": {
52
+ const { runPrune } = await import("./commands/prune");
53
+ await runPrune();
51
54
  break;
52
55
  }
53
56
  case "version":
@@ -29,7 +29,7 @@ function _formatSize(bytes: number): string {
29
29
  export async function runHealth(): Promise<void> {
30
30
  const skills = scanInstalledSkills();
31
31
  const _skillsDir = join(homedir(), ".claude", "skills");
32
- const dbPath = join(homedir(), ".skill-kit", "analytics.db");
32
+ const dbPath = join(homedir(), ".skillkit", "analytics.db");
33
33
  const dbExists = existsSync(dbPath);
34
34
 
35
35
  let totalContextChars = 0;
@@ -83,10 +83,10 @@ export async function runHealth(): Promise<void> {
83
83
  );
84
84
  } else if (dbExists) {
85
85
  console.log(warn("Analytics DB exists but has no data"));
86
- console.log(dim(" Run: skill-kit stats"));
86
+ console.log(dim(" Run: skillkit scan"));
87
87
  } else {
88
88
  console.log(warn("Analytics DB not found"));
89
- console.log(dim(" Run: skill-kit stats"));
89
+ console.log(dim(" Run: skillkit scan"));
90
90
  }
91
91
 
92
92
  if (neverUsed.length > 0) {
@@ -95,7 +95,7 @@ export async function runHealth(): Promise<void> {
95
95
  const preview = neverUsed.slice(0, 5).join(", ");
96
96
  const more = neverUsed.length > 5 ? ` +${neverUsed.length - 5} more` : "";
97
97
  console.log(` ${dim(preview + more)}`);
98
- console.log(` ${dim("Run: skill-kit uninstall <name>")}`);
98
+ console.log(` ${dim("Run: skillkit prune")}`);
99
99
  }
100
100
 
101
101
  const unusedContextChars = neverUsed.reduce((acc, name) => {
@@ -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
+ }
@@ -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: skill-kit analyze")}\n`);
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(), ".skill-kit");
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
@@ -1,5 +1,7 @@
1
1
  export { runHealth } from "./commands/health";
2
2
  export { runList } from "./commands/list";
3
+ export { runPrune } from "./commands/prune";
4
+ export { runScan } from "./commands/scan";
3
5
  export { runStats } from "./commands/stats";
4
6
  export {
5
7
  getDailyUsage,
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src",
6
- "types": ["bun-types"]
7
- },
8
- "include": ["src"]
9
- }