@crafter/skillkit 0.1.7 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crafter/skillkit",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
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,12 @@
1
1
  #!/usr/bin/env bun
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
2
4
  import { bold, cyan, dim, yellow } from "./tui/colors";
3
5
 
4
- const VERSION = "0.1.7";
6
+ const pkg = JSON.parse(
7
+ readFileSync(join(dirname(import.meta.dir), "package.json"), "utf-8"),
8
+ );
9
+ const VERSION: string = pkg.version;
5
10
 
6
11
  function printHelp(): void {
7
12
  console.log(`
@@ -115,8 +115,10 @@ export async function runHealth(): Promise<void> {
115
115
  console.log(`\n ${bold("SKILLKIT HEALTH REPORT")}\n`);
116
116
 
117
117
  const agents = getDetectedAgents();
118
- console.log(check(`${skills.length} skills across ${agents.length} agents`));
119
- console.log(dim(` ${agents.join(", ")}`));
118
+ console.log(check(`${skills.length} skills installed`));
119
+ if (agents.length > 0) {
120
+ console.log(dim(` ${agents.join(" + ")}`));
121
+ }
120
122
 
121
123
  if (dbExists && hasDbData) {
122
124
  console.log(
@@ -20,11 +20,6 @@ export function runList(): void {
20
20
  return;
21
21
  }
22
22
 
23
- const agentCounts = new Map<string, number>();
24
- for (const s of skills) {
25
- agentCounts.set(s.agent, (agentCounts.get(s.agent) ?? 0) + 1);
26
- }
27
-
28
23
  const totalSize = skills.reduce((acc, s) => acc + s.size, 0);
29
24
 
30
25
  console.log(`\n ${bold(`INSTALLED SKILLS (${skills.length})`)}\n`);
@@ -45,10 +40,7 @@ export function runList(): void {
45
40
  console.log(` ${name}${desc}${size}`);
46
41
  }
47
42
 
48
- const agentSummary = [...agentCounts.entries()]
49
- .map(([a, c]) => `${a} (${c})`)
50
- .join(", ");
51
43
  console.log(
52
- `\n ${dim(`Total: ${skills.length} skills | ${formatSize(totalSize)} | ${agentSummary}`)}\n`,
44
+ `\n ${dim(`Total: ${skills.length} skills | ${formatSize(totalSize)}`)}\n`,
53
45
  );
54
46
  }
@@ -1,8 +1,8 @@
1
1
  import { existsSync, readdirSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { upsertInstalledSkill } from "../db/queries";
3
+ import { upsertInstalledSkill, deduplicateInvocations } from "../db/queries";
4
4
  import { getDb } from "../db/schema";
5
- import { scanAllSessions, countAllSessions } from "../scanner/index";
5
+ import { countAllSessions, scanAllSessions } from "../scanner/index";
6
6
  import { getDetectedAgents, scanInstalledSkills } from "../scanner/skills";
7
7
  import { bold, cyan, dim } from "../tui/colors";
8
8
 
@@ -26,10 +26,12 @@ export async function runScan(): Promise<void> {
26
26
 
27
27
  const agents = getDetectedAgents();
28
28
  if (agents.length === 0) {
29
- console.log(`\n ${dim("No agent skill directories found.")}\n`);
29
+ console.log(
30
+ `\n ${dim("No supported agents found (Claude Code, OpenCode).")}\n`,
31
+ );
30
32
  return;
31
33
  }
32
- console.log(`\n ${dim(`Scanning ${agents.length} agents: ${agents.join(", ")}`)}`);
34
+ console.log(`\n ${dim(`Scanning ${agents.join(" + ")}`)}`);
33
35
 
34
36
  const skills = scanInstalledSkills();
35
37
 
@@ -84,6 +86,11 @@ export async function runScan(): Promise<void> {
84
86
  } catch {}
85
87
  }
86
88
 
89
+ const removed = deduplicateInvocations(db);
90
+ if (removed > 0) {
91
+ console.log(` ${dim(`Cleaned ${removed} duplicate entries`)}`);
92
+ }
93
+
87
94
  console.log(dim(" Scanning sessions..."));
88
95
 
89
96
  const sessionCount = countAllSessions();
package/src/db/queries.ts CHANGED
@@ -76,18 +76,53 @@ export function recordInvocation(
76
76
  skillName: string,
77
77
  sessionId?: string,
78
78
  project?: string,
79
+ timestamp?: string,
79
80
  ): void {
81
+ const ts = timestamp ?? new Date().toISOString();
80
82
  db.run(
81
83
  "INSERT INTO skill_invocations (skill_name, timestamp, session_id, project) VALUES (?, ?, ?, ?)",
82
- [skillName, new Date().toISOString(), sessionId ?? null, project ?? null],
84
+ [skillName, ts, sessionId ?? null, project ?? null],
83
85
  );
84
- const date = new Date().toISOString().slice(0, 10);
86
+ const date = ts.slice(0, 10);
85
87
  db.run(
86
88
  "INSERT INTO skill_daily_stats (date, skill_name, count) VALUES (?, ?, 1) ON CONFLICT(date, skill_name) DO UPDATE SET count = count + 1",
87
89
  [date, skillName],
88
90
  );
89
91
  }
90
92
 
93
+ export function deduplicateInvocations(db: Database): number {
94
+ const before = db
95
+ .query<{ count: number }, []>(
96
+ "SELECT COUNT(*) as count FROM skill_invocations",
97
+ )
98
+ .get()?.count ?? 0;
99
+
100
+ db.run(`
101
+ DELETE FROM skill_invocations WHERE id NOT IN (
102
+ SELECT MIN(id) FROM skill_invocations
103
+ GROUP BY skill_name, session_id, timestamp
104
+ )
105
+ `);
106
+
107
+ const after = db
108
+ .query<{ count: number }, []>(
109
+ "SELECT COUNT(*) as count FROM skill_invocations",
110
+ )
111
+ .get()?.count ?? 0;
112
+
113
+ if (before !== after) {
114
+ db.run("DELETE FROM skill_daily_stats");
115
+ db.run(`
116
+ INSERT INTO skill_daily_stats (date, skill_name, count)
117
+ SELECT date(timestamp), skill_name, COUNT(*)
118
+ FROM skill_invocations
119
+ GROUP BY date(timestamp), skill_name
120
+ `);
121
+ }
122
+
123
+ return before - after;
124
+ }
125
+
91
126
  export function upsertInstalledSkill(
92
127
  db: Database,
93
128
  name: string,
package/src/index.ts CHANGED
@@ -12,8 +12,8 @@ export {
12
12
  upsertInstalledSkill,
13
13
  } from "./db/queries";
14
14
  export { getDb } from "./db/schema";
15
- export { scanAllSessions, countAllSessions } from "./scanner/index";
16
15
  export { parseSessionFile } from "./scanner/connectors/claude";
16
+ export { countAllSessions, scanAllSessions } from "./scanner/index";
17
17
  export { getDetectedAgents, scanInstalledSkills } from "./scanner/skills";
18
18
  export type {
19
19
  InstalledSkill,
@@ -51,8 +51,7 @@ export function parseSessionFile(filePath: string): Invocation[] {
51
51
  const msg = obj.message as
52
52
  | { content: Array<Record<string, unknown>> }
53
53
  | undefined;
54
- const msgContent =
55
- obj.type === "assistant" && msg ? msg.content : null;
54
+ const msgContent = obj.type === "assistant" && msg ? msg.content : null;
56
55
 
57
56
  if (!Array.isArray(msgContent)) continue;
58
57
 
@@ -63,9 +62,7 @@ export function parseSessionFile(filePath: string): Invocation[] {
63
62
  block.type === "tool_use" &&
64
63
  (block as unknown as ToolUseBlock).name === "Skill"
65
64
  ) {
66
- const skillName = extractSkillName(
67
- block as unknown as ToolUseBlock,
68
- );
65
+ const skillName = extractSkillName(block as unknown as ToolUseBlock);
69
66
  if (skillName) {
70
67
  results.push({ skillName, timestamp, sessionId });
71
68
  }
@@ -1,10 +1,10 @@
1
- import { Database as BunDatabase } from "bun:sqlite";
2
1
  import type { Database } from "bun:sqlite";
2
+ import { Database as BunDatabase } from "bun:sqlite";
3
3
  import { existsSync } from "node:fs";
4
4
  import { homedir, platform } from "node:os";
5
5
  import { join } from "node:path";
6
- import { recordNewInvocations } from "../index";
7
6
  import type { Invocation } from "../index";
7
+ import { recordNewInvocations } from "../index";
8
8
 
9
9
  function getDbPath(): string | null {
10
10
  const os = platform();
@@ -48,9 +48,7 @@ export function countOpenCodeSessions(): number {
48
48
 
49
49
  try {
50
50
  const row = ocDb
51
- .query<{ count: number }, []>(
52
- "SELECT COUNT(*) as count FROM session",
53
- )
51
+ .query<{ count: number }, []>("SELECT COUNT(*) as count FROM session")
54
52
  .get();
55
53
  return row?.count ?? 0;
56
54
  } catch {
@@ -70,7 +68,7 @@ export function scanOpenCodeSessions(
70
68
  try {
71
69
  const rows = ocDb
72
70
  .query<PartRow, []>(
73
- "SELECT p.session_id, p.time_created, p.data FROM part p WHERE p.data LIKE '%\"tool\":\"skill\"%'",
71
+ 'SELECT p.session_id, p.time_created, p.data FROM part p WHERE p.data LIKE \'%"tool":"skill"%\'',
74
72
  )
75
73
  .all();
76
74
 
@@ -1,9 +1,9 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import { recordInvocation } from "../db/queries";
3
- import { scanClaudeSessions, countClaudeSessions } from "./connectors/claude";
3
+ import { countClaudeSessions, scanClaudeSessions } from "./connectors/claude";
4
4
  import {
5
- scanOpenCodeSessions,
6
5
  countOpenCodeSessions,
6
+ scanOpenCodeSessions,
7
7
  } from "./connectors/opencode";
8
8
 
9
9
  interface AlreadyTracked {
@@ -35,7 +35,7 @@ export function recordNewInvocations(
35
35
  for (const inv of invocations) {
36
36
  const key = `${inv.sessionId}::${inv.timestamp}`;
37
37
  if (!trackedSet.has(key)) {
38
- recordInvocation(db, inv.skillName, inv.sessionId);
38
+ recordInvocation(db, inv.skillName, inv.sessionId, undefined, inv.timestamp);
39
39
  trackedSet.add(key);
40
40
  count++;
41
41
  }
@@ -3,22 +3,23 @@ import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { InstalledSkill } from "../types";
5
5
 
6
- const AGENT_SKILL_PATHS: Array<{ agent: string; dir: string }> = [
6
+ const SUPPORTED_AGENTS: Array<{ agent: string; dir: string }> = [
7
7
  { agent: "Claude Code", dir: join(homedir(), ".claude", "skills") },
8
- { agent: "Cursor", dir: join(homedir(), ".cursor", "skills") },
9
- { agent: "Codex", dir: join(homedir(), ".codex", "skills") },
10
- { agent: "Windsurf", dir: join(homedir(), ".codeium", "windsurf", "skills") },
11
- { agent: "Gemini CLI", dir: join(homedir(), ".gemini", "skills") },
12
- { agent: "Cline", dir: join(homedir(), ".cline", "skills") },
13
- { agent: "Roo Code", dir: join(homedir(), ".roo", "skills") },
14
- { agent: "Continue", dir: join(homedir(), ".continue", "skills") },
15
8
  { agent: "OpenCode", dir: join(homedir(), ".config", "opencode", "skills") },
16
- { agent: "GitHub Copilot", dir: join(homedir(), ".copilot", "skills") },
17
- { agent: "OpenHands", dir: join(homedir(), ".openhands", "skills") },
18
- { agent: "Amp", dir: join(homedir(), ".config", "agents", "skills") },
19
- { agent: "Goose", dir: join(homedir(), ".config", "goose", "skills") },
20
- { agent: "Kilo Code", dir: join(homedir(), ".kilocode", "skills") },
21
- { agent: "Trae", dir: join(homedir(), ".trae", "skills") },
9
+ // Planned needs session connector to enable full analytics pipeline
10
+ // { agent: "Cursor", dir: join(homedir(), ".cursor", "skills") }, // GH-1: injects skills as context rules, no discrete tool_use
11
+ // { agent: "Codex", dir: join(homedir(), ".codex", "skills") }, // GH-2
12
+ // { agent: "Windsurf", dir: join(homedir(), ".codeium", "windsurf", "skills") }, // GH-3
13
+ // { agent: "Gemini CLI", dir: join(homedir(), ".gemini", "skills") }, // GH-4
14
+ // { agent: "Cline", dir: join(homedir(), ".cline", "skills") }, // GH-5
15
+ // { agent: "Roo Code", dir: join(homedir(), ".roo", "skills") }, // GH-6
16
+ // { agent: "Continue", dir: join(homedir(), ".continue", "skills") }, // GH-7
17
+ // { agent: "GitHub Copilot", dir: join(homedir(), ".copilot", "skills") }, // GH-8
18
+ // { agent: "OpenHands", dir: join(homedir(), ".openhands", "skills") }, // GH-9
19
+ // { agent: "Amp", dir: join(homedir(), ".config", "agents", "skills") }, // GH-10
20
+ // { agent: "Goose", dir: join(homedir(), ".config", "goose", "skills") }, // GH-11
21
+ // { agent: "Kilo Code", dir: join(homedir(), ".kilocode", "skills") }, // GH-12
22
+ // { agent: "Trae", dir: join(homedir(), ".trae", "skills") }, // GH-13
22
23
  ];
23
24
 
24
25
  function parseYamlFrontmatter(content: string): Record<string, string> {
@@ -56,10 +57,7 @@ function getDirSize(dirPath: string): number {
56
57
  return total;
57
58
  }
58
59
 
59
- function scanSkillsDir(
60
- skillsDir: string,
61
- agent: string,
62
- ): InstalledSkill[] {
60
+ function scanSkillsDir(skillsDir: string, agent: string): InstalledSkill[] {
63
61
  if (!existsSync(skillsDir)) return [];
64
62
 
65
63
  const skills: InstalledSkill[] = [];
@@ -125,7 +123,7 @@ export function scanInstalledSkills(): InstalledSkill[] {
125
123
  const allSkills: InstalledSkill[] = [];
126
124
  const seen = new Set<string>();
127
125
 
128
- for (const { agent, dir } of AGENT_SKILL_PATHS) {
126
+ for (const { agent, dir } of SUPPORTED_AGENTS) {
129
127
  const skills = scanSkillsDir(dir, agent);
130
128
  for (const skill of skills) {
131
129
  try {
@@ -143,7 +141,7 @@ export function scanInstalledSkills(): InstalledSkill[] {
143
141
 
144
142
  export function getDetectedAgents(): string[] {
145
143
  const agents: string[] = [];
146
- for (const { agent, dir } of AGENT_SKILL_PATHS) {
144
+ for (const { agent, dir } of SUPPORTED_AGENTS) {
147
145
  if (existsSync(dir)) agents.push(agent);
148
146
  }
149
147
  return agents;