@crafter/skillkit 0.2.0 → 0.2.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@crafter/skillkit",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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": {
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readdirSync, statSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { basename, join } from "node:path";
3
3
  import { upsertInstalledSkill, deduplicateInvocations } from "../db/queries";
4
4
  import { getDb } from "../db/schema";
5
5
  import { countAllSessions, scanAllSessions } from "../scanner/index";
@@ -86,6 +86,23 @@ export async function runScan(): Promise<void> {
86
86
  } catch {}
87
87
  }
88
88
 
89
+ const knownSkills = new Set<string>();
90
+ for (const skill of skills) {
91
+ knownSkills.add(skill.name);
92
+ knownSkills.add(basename(skill.path));
93
+ }
94
+ if (existsSync(localSkillsDir)) {
95
+ try {
96
+ for (const e of readdirSync(localSkillsDir)) {
97
+ try {
98
+ if (statSync(join(localSkillsDir, e)).isDirectory()) {
99
+ knownSkills.add(e);
100
+ }
101
+ } catch {}
102
+ }
103
+ } catch {}
104
+ }
105
+
89
106
  const removed = deduplicateInvocations(db);
90
107
  if (removed > 0) {
91
108
  console.log(` ${dim(`Cleaned ${removed} duplicate entries`)}`);
@@ -94,7 +111,7 @@ export async function runScan(): Promise<void> {
94
111
  console.log(dim(" Scanning sessions..."));
95
112
 
96
113
  const sessionCount = countAllSessions();
97
- const newInvocations = await scanAllSessions(db);
114
+ const newInvocations = await scanAllSessions(db, knownSkills);
98
115
  const totalRow = db
99
116
  .query<{ count: number }, []>(
100
117
  "SELECT COUNT(*) as count FROM skill_invocations",
@@ -19,7 +19,28 @@ function extractSkillName(block: ToolUseBlock): string | null {
19
19
  return null;
20
20
  }
21
21
 
22
- export function parseSessionFile(filePath: string): Invocation[] {
22
+ const COMMAND_NAME_RE = /<command-name>\/?([a-zA-Z][\w-]*(?::[\w-]*)*)<\/command-name>/g;
23
+
24
+ function extractCommandNames(
25
+ text: string,
26
+ knownSkills: Set<string>,
27
+ ): string[] {
28
+ const names: string[] = [];
29
+ let match: RegExpExecArray | null;
30
+ while ((match = COMMAND_NAME_RE.exec(text)) !== null) {
31
+ const name = match[1];
32
+ if (knownSkills.has(name)) {
33
+ names.push(name);
34
+ }
35
+ }
36
+ COMMAND_NAME_RE.lastIndex = 0;
37
+ return names;
38
+ }
39
+
40
+ export function parseSessionFile(
41
+ filePath: string,
42
+ knownSkills: Set<string> = new Set(),
43
+ ): Invocation[] {
23
44
  const results: Invocation[] = [];
24
45
  const sessionId = basename(filePath, ".jsonl");
25
46
 
@@ -49,9 +70,35 @@ export function parseSessionFile(filePath: string): Invocation[] {
49
70
  : new Date().toISOString();
50
71
 
51
72
  const msg = obj.message as
52
- | { content: Array<Record<string, unknown>> }
73
+ | { content: Array<Record<string, unknown>> | string }
53
74
  | undefined;
54
- const msgContent = obj.type === "assistant" && msg ? msg.content : null;
75
+
76
+ if (obj.type === "user" && msg) {
77
+ const text =
78
+ typeof msg.content === "string"
79
+ ? msg.content
80
+ : Array.isArray(msg.content)
81
+ ? msg.content
82
+ .filter(
83
+ (b): b is { type: string; text: string } =>
84
+ typeof b === "object" &&
85
+ b !== null &&
86
+ b.type === "text" &&
87
+ typeof b.text === "string",
88
+ )
89
+ .map((b) => b.text)
90
+ .join("\n")
91
+ : "";
92
+ for (const name of extractCommandNames(text, knownSkills)) {
93
+ results.push({ skillName: name, timestamp, sessionId });
94
+ }
95
+ continue;
96
+ }
97
+
98
+ const msgContent =
99
+ obj.type === "assistant" && msg && Array.isArray(msg.content)
100
+ ? (msg.content as Array<Record<string, unknown>>)
101
+ : null;
55
102
 
56
103
  if (!Array.isArray(msgContent)) continue;
57
104
 
@@ -88,6 +135,7 @@ export function countClaudeSessions(): number {
88
135
  export async function scanClaudeSessions(
89
136
  db: Database,
90
137
  trackedSet: Set<string>,
138
+ knownSkills: Set<string> = new Set(),
91
139
  ): Promise<number> {
92
140
  const projectsDir = join(homedir(), ".claude", "projects");
93
141
  if (!existsSync(projectsDir)) return 0;
@@ -101,7 +149,7 @@ export async function scanClaudeSessions(
101
149
 
102
150
  let total = 0;
103
151
  for (const file of files) {
104
- const invocations = parseSessionFile(file);
152
+ const invocations = parseSessionFile(file, knownSkills);
105
153
  total += recordNewInvocations(db, trackedSet, invocations);
106
154
  }
107
155
 
@@ -43,10 +43,13 @@ export function recordNewInvocations(
43
43
  return count;
44
44
  }
45
45
 
46
- export async function scanAllSessions(db: Database): Promise<number> {
46
+ export async function scanAllSessions(
47
+ db: Database,
48
+ knownSkills: Set<string> = new Set(),
49
+ ): Promise<number> {
47
50
  const trackedSet = getTrackedSet(db);
48
51
  let total = 0;
49
- total += await scanClaudeSessions(db, trackedSet);
52
+ total += await scanClaudeSessions(db, trackedSet, knownSkills);
50
53
  total += scanOpenCodeSessions(db, trackedSet);
51
54
  return total;
52
55
  }