@crafter/skillkit 0.1.4 → 0.1.6
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/README.md +7 -8
- package/package.json +1 -1
- package/src/bin.ts +1 -1
- package/src/commands/health.ts +4 -2
- package/src/commands/list.ts +10 -2
- package/src/commands/scan.ts +9 -17
- package/src/index.ts +3 -2
- package/src/scanner/connectors/claude.ts +112 -0
- package/src/scanner/connectors/opencode.ts +116 -0
- package/src/scanner/index.ts +38 -111
- package/src/scanner/skills.ts +52 -4
- package/src/types/index.ts +1 -0
package/README.md
CHANGED
|
@@ -99,14 +99,13 @@ All data stays on your machine. No telemetry. No signup.
|
|
|
99
99
|
|
|
100
100
|
## Supported Agents
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
- Claude Code
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
- Gemini CLI
|
|
102
|
+
Scans skill directories for 15+ agents automatically:
|
|
103
|
+
|
|
104
|
+
- Claude Code, Cursor, Codex, Windsurf, Gemini CLI
|
|
105
|
+
- Cline, Roo Code, Continue, OpenCode, GitHub Copilot
|
|
106
|
+
- OpenHands, Amp, Goose, Kilo Code, Trae
|
|
107
|
+
|
|
108
|
+
Skills installed via [skills.sh](https://skills.sh) symlinks are deduplicated across agents.
|
|
110
109
|
|
|
111
110
|
## License
|
|
112
111
|
|
package/package.json
CHANGED
package/src/bin.ts
CHANGED
package/src/commands/health.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getTopSkills } from "../db/queries";
|
|
5
5
|
import { getDb } from "../db/schema";
|
|
6
|
-
import { scanInstalledSkills } from "../scanner/skills";
|
|
6
|
+
import { getDetectedAgents, scanInstalledSkills } from "../scanner/skills";
|
|
7
7
|
import { bold, dim, green, red, yellow } from "../tui/colors";
|
|
8
8
|
|
|
9
9
|
const METADATA_BUDGET = 16000;
|
|
@@ -114,7 +114,9 @@ export async function runHealth(): Promise<void> {
|
|
|
114
114
|
|
|
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
120
|
|
|
119
121
|
if (dbExists && hasDbData) {
|
|
120
122
|
console.log(
|
package/src/commands/list.ts
CHANGED
|
@@ -16,10 +16,15 @@ export function runList(): void {
|
|
|
16
16
|
const skills = scanInstalledSkills();
|
|
17
17
|
|
|
18
18
|
if (skills.length === 0) {
|
|
19
|
-
console.log("\n No skills
|
|
19
|
+
console.log("\n No skills found.\n");
|
|
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
|
+
|
|
23
28
|
const totalSize = skills.reduce((acc, s) => acc + s.size, 0);
|
|
24
29
|
|
|
25
30
|
console.log(`\n ${bold(`INSTALLED SKILLS (${skills.length})`)}\n`);
|
|
@@ -40,7 +45,10 @@ export function runList(): void {
|
|
|
40
45
|
console.log(` ${name}${desc}${size}`);
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
const agentSummary = [...agentCounts.entries()]
|
|
49
|
+
.map(([a, c]) => `${a} (${c})`)
|
|
50
|
+
.join(", ");
|
|
43
51
|
console.log(
|
|
44
|
-
`\n ${dim(`Total: ${skills.length} skills | ${formatSize(totalSize)}
|
|
52
|
+
`\n ${dim(`Total: ${skills.length} skills | ${formatSize(totalSize)} | ${agentSummary}`)}\n`,
|
|
45
53
|
);
|
|
46
54
|
}
|
package/src/commands/scan.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { upsertInstalledSkill } from "../db/queries";
|
|
5
4
|
import { getDb } from "../db/schema";
|
|
6
|
-
import { scanAllSessions } from "../scanner/index";
|
|
7
|
-
import { scanInstalledSkills } from "../scanner/skills";
|
|
5
|
+
import { scanAllSessions, countAllSessions } from "../scanner/index";
|
|
6
|
+
import { getDetectedAgents, scanInstalledSkills } from "../scanner/skills";
|
|
8
7
|
import { bold, cyan, dim } from "../tui/colors";
|
|
9
8
|
|
|
10
9
|
function detectSource(skillPath: string): "skills.sh" | "manual" {
|
|
@@ -22,22 +21,15 @@ function detectSource(skillPath: string): "skills.sh" | "manual" {
|
|
|
22
21
|
return "manual";
|
|
23
22
|
}
|
|
24
23
|
|
|
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
24
|
export async function runScan(): Promise<void> {
|
|
38
25
|
const db = getDb();
|
|
39
26
|
|
|
40
|
-
|
|
27
|
+
const agents = getDetectedAgents();
|
|
28
|
+
if (agents.length === 0) {
|
|
29
|
+
console.log(`\n ${dim("No agent skill directories found.")}\n`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
console.log(`\n ${dim(`Scanning ${agents.length} agents: ${agents.join(", ")}`)}`);
|
|
41
33
|
|
|
42
34
|
const skills = scanInstalledSkills();
|
|
43
35
|
|
|
@@ -94,7 +86,7 @@ export async function runScan(): Promise<void> {
|
|
|
94
86
|
|
|
95
87
|
console.log(dim(" Scanning sessions..."));
|
|
96
88
|
|
|
97
|
-
const sessionCount =
|
|
89
|
+
const sessionCount = countAllSessions();
|
|
98
90
|
const newInvocations = await scanAllSessions(db);
|
|
99
91
|
const totalRow = db
|
|
100
92
|
.query<{ count: number }, []>(
|
package/src/index.ts
CHANGED
|
@@ -12,8 +12,9 @@ export {
|
|
|
12
12
|
upsertInstalledSkill,
|
|
13
13
|
} from "./db/queries";
|
|
14
14
|
export { getDb } from "./db/schema";
|
|
15
|
-
export {
|
|
16
|
-
export {
|
|
15
|
+
export { scanAllSessions, countAllSessions } from "./scanner/index";
|
|
16
|
+
export { parseSessionFile } from "./scanner/connectors/claude";
|
|
17
|
+
export { getDetectedAgents, scanInstalledSkills } from "./scanner/skills";
|
|
17
18
|
export type {
|
|
18
19
|
InstalledSkill,
|
|
19
20
|
SkillInvocation,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
5
|
+
import type { Invocation } from "../index";
|
|
6
|
+
import { recordNewInvocations } from "../index";
|
|
7
|
+
|
|
8
|
+
interface ToolUseBlock {
|
|
9
|
+
type: "tool_use";
|
|
10
|
+
name: string;
|
|
11
|
+
input: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function extractSkillName(block: ToolUseBlock): string | null {
|
|
15
|
+
const input = block.input;
|
|
16
|
+
if (typeof input.skill === "string") return input.skill;
|
|
17
|
+
if (typeof input.name === "string") return input.name;
|
|
18
|
+
if (typeof input.skillName === "string") return input.skillName;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseSessionFile(filePath: string): Invocation[] {
|
|
23
|
+
const results: Invocation[] = [];
|
|
24
|
+
const sessionId = basename(filePath, ".jsonl");
|
|
25
|
+
|
|
26
|
+
let content: string;
|
|
27
|
+
try {
|
|
28
|
+
content = readFileSync(filePath, "utf-8");
|
|
29
|
+
} catch {
|
|
30
|
+
return results;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const lines = content.split("\n");
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
if (!line.trim()) continue;
|
|
36
|
+
let entry: unknown;
|
|
37
|
+
try {
|
|
38
|
+
entry = JSON.parse(line);
|
|
39
|
+
} catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
44
|
+
const obj = entry as Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
const timestamp =
|
|
47
|
+
typeof obj.timestamp === "string"
|
|
48
|
+
? obj.timestamp
|
|
49
|
+
: new Date().toISOString();
|
|
50
|
+
|
|
51
|
+
const msg = obj.message as
|
|
52
|
+
| { content: Array<Record<string, unknown>> }
|
|
53
|
+
| undefined;
|
|
54
|
+
const msgContent =
|
|
55
|
+
obj.type === "assistant" && msg ? msg.content : null;
|
|
56
|
+
|
|
57
|
+
if (!Array.isArray(msgContent)) continue;
|
|
58
|
+
|
|
59
|
+
for (const block of msgContent) {
|
|
60
|
+
if (
|
|
61
|
+
typeof block === "object" &&
|
|
62
|
+
block !== null &&
|
|
63
|
+
block.type === "tool_use" &&
|
|
64
|
+
(block as unknown as ToolUseBlock).name === "Skill"
|
|
65
|
+
) {
|
|
66
|
+
const skillName = extractSkillName(
|
|
67
|
+
block as unknown as ToolUseBlock,
|
|
68
|
+
);
|
|
69
|
+
if (skillName) {
|
|
70
|
+
results.push({ skillName, timestamp, sessionId });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function countClaudeSessions(): number {
|
|
80
|
+
const projectsDir = join(homedir(), ".claude", "projects");
|
|
81
|
+
if (!existsSync(projectsDir)) return 0;
|
|
82
|
+
|
|
83
|
+
let count = 0;
|
|
84
|
+
const glob = new Bun.Glob("**/*.jsonl");
|
|
85
|
+
for (const _ of glob.scanSync({ cwd: projectsDir })) {
|
|
86
|
+
count++;
|
|
87
|
+
}
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function scanClaudeSessions(
|
|
92
|
+
db: Database,
|
|
93
|
+
trackedSet: Set<string>,
|
|
94
|
+
): Promise<number> {
|
|
95
|
+
const projectsDir = join(homedir(), ".claude", "projects");
|
|
96
|
+
if (!existsSync(projectsDir)) return 0;
|
|
97
|
+
|
|
98
|
+
const glob = new Bun.Glob("**/*.jsonl");
|
|
99
|
+
const files: string[] = [];
|
|
100
|
+
|
|
101
|
+
for await (const file of glob.scan({ cwd: projectsDir, absolute: true })) {
|
|
102
|
+
files.push(file);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let total = 0;
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
const invocations = parseSessionFile(file);
|
|
108
|
+
total += recordNewInvocations(db, trackedSet, invocations);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return total;
|
|
112
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
2
|
+
import type { Database } from "bun:sqlite";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { homedir, platform } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { recordNewInvocations } from "../index";
|
|
7
|
+
import type { Invocation } from "../index";
|
|
8
|
+
|
|
9
|
+
function getDbPath(): string | null {
|
|
10
|
+
const os = platform();
|
|
11
|
+
let dataDir: string;
|
|
12
|
+
if (os === "darwin") {
|
|
13
|
+
dataDir = join(homedir(), "Library", "Application Support", "opencode");
|
|
14
|
+
} else if (os === "win32") {
|
|
15
|
+
dataDir = join(
|
|
16
|
+
process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"),
|
|
17
|
+
"opencode",
|
|
18
|
+
);
|
|
19
|
+
} else {
|
|
20
|
+
dataDir = join(
|
|
21
|
+
process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"),
|
|
22
|
+
"opencode",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const dbPath = join(dataDir, "opencode.db");
|
|
26
|
+
return existsSync(dbPath) ? dbPath : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function openDb(): InstanceType<typeof BunDatabase> | null {
|
|
30
|
+
const dbPath = getDbPath();
|
|
31
|
+
if (!dbPath) return null;
|
|
32
|
+
try {
|
|
33
|
+
return new BunDatabase(dbPath, { readonly: true });
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface PartRow {
|
|
40
|
+
session_id: string;
|
|
41
|
+
time_created: number;
|
|
42
|
+
data: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function countOpenCodeSessions(): number {
|
|
46
|
+
const ocDb = openDb();
|
|
47
|
+
if (!ocDb) return 0;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const row = ocDb
|
|
51
|
+
.query<{ count: number }, []>(
|
|
52
|
+
"SELECT COUNT(*) as count FROM session",
|
|
53
|
+
)
|
|
54
|
+
.get();
|
|
55
|
+
return row?.count ?? 0;
|
|
56
|
+
} catch {
|
|
57
|
+
return 0;
|
|
58
|
+
} finally {
|
|
59
|
+
ocDb.close();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function scanOpenCodeSessions(
|
|
64
|
+
db: Database,
|
|
65
|
+
trackedSet: Set<string>,
|
|
66
|
+
): number {
|
|
67
|
+
const ocDb = openDb();
|
|
68
|
+
if (!ocDb) return 0;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const rows = ocDb
|
|
72
|
+
.query<PartRow, []>(
|
|
73
|
+
"SELECT p.session_id, p.time_created, p.data FROM part p WHERE p.data LIKE '%\"tool\":\"skill\"%'",
|
|
74
|
+
)
|
|
75
|
+
.all();
|
|
76
|
+
|
|
77
|
+
const invocations: Invocation[] = [];
|
|
78
|
+
|
|
79
|
+
for (const row of rows) {
|
|
80
|
+
let data: Record<string, unknown>;
|
|
81
|
+
try {
|
|
82
|
+
data = JSON.parse(row.data);
|
|
83
|
+
} catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (data.type !== "tool" || data.tool !== "skill") continue;
|
|
88
|
+
|
|
89
|
+
const state = data.state as Record<string, unknown> | undefined;
|
|
90
|
+
if (!state) continue;
|
|
91
|
+
|
|
92
|
+
const input = state.input as Record<string, unknown> | undefined;
|
|
93
|
+
if (!input) continue;
|
|
94
|
+
|
|
95
|
+
const skillName =
|
|
96
|
+
typeof input.name === "string"
|
|
97
|
+
? input.name
|
|
98
|
+
: typeof input.skill === "string"
|
|
99
|
+
? input.skill
|
|
100
|
+
: null;
|
|
101
|
+
if (!skillName) continue;
|
|
102
|
+
|
|
103
|
+
invocations.push({
|
|
104
|
+
skillName,
|
|
105
|
+
timestamp: new Date(row.time_created).toISOString(),
|
|
106
|
+
sessionId: `oc:${row.session_id}`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return recordNewInvocations(db, trackedSet, invocations);
|
|
111
|
+
} catch {
|
|
112
|
+
return 0;
|
|
113
|
+
} finally {
|
|
114
|
+
ocDb.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/scanner/index.ts
CHANGED
|
@@ -1,129 +1,56 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { basename, join } from "node:path";
|
|
5
2
|
import { recordInvocation } from "../db/queries";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface AssistantMessage {
|
|
14
|
-
type: "assistant";
|
|
15
|
-
message: {
|
|
16
|
-
content: Array<ToolUseBlock | { type: string }>;
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function extractSessionId(filePath: string): string {
|
|
21
|
-
return basename(filePath, ".jsonl");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function extractSkillName(block: ToolUseBlock): string | null {
|
|
25
|
-
const input = block.input;
|
|
26
|
-
if (typeof input.skill === "string") return input.skill;
|
|
27
|
-
if (typeof input.name === "string") return input.name;
|
|
28
|
-
if (typeof input.skillName === "string") return input.skillName;
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function parseSessionFile(
|
|
33
|
-
filePath: string,
|
|
34
|
-
): Array<{ skillName: string; timestamp: string; sessionId: string }> {
|
|
35
|
-
const results: Array<{
|
|
36
|
-
skillName: string;
|
|
37
|
-
timestamp: string;
|
|
38
|
-
sessionId: string;
|
|
39
|
-
}> = [];
|
|
40
|
-
const sessionId = extractSessionId(filePath);
|
|
41
|
-
|
|
42
|
-
let content: string;
|
|
43
|
-
try {
|
|
44
|
-
content = readFileSync(filePath, "utf-8");
|
|
45
|
-
} catch {
|
|
46
|
-
return results;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const lines = content.split("\n");
|
|
50
|
-
for (const line of lines) {
|
|
51
|
-
if (!line.trim()) continue;
|
|
52
|
-
let entry: unknown;
|
|
53
|
-
try {
|
|
54
|
-
entry = JSON.parse(line);
|
|
55
|
-
} catch {
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (typeof entry !== "object" || entry === null) continue;
|
|
60
|
-
const obj = entry as Record<string, unknown>;
|
|
61
|
-
|
|
62
|
-
const timestamp =
|
|
63
|
-
typeof obj.timestamp === "string"
|
|
64
|
-
? obj.timestamp
|
|
65
|
-
: new Date().toISOString();
|
|
66
|
-
|
|
67
|
-
const msg = obj.message as AssistantMessage["message"] | undefined;
|
|
68
|
-
const content = obj.type === "assistant" && msg ? msg.content : null;
|
|
69
|
-
|
|
70
|
-
if (!Array.isArray(content)) continue;
|
|
71
|
-
|
|
72
|
-
for (const block of content) {
|
|
73
|
-
if (
|
|
74
|
-
typeof block === "object" &&
|
|
75
|
-
block !== null &&
|
|
76
|
-
(block as Record<string, unknown>).type === "tool_use" &&
|
|
77
|
-
(block as ToolUseBlock).name === "Skill"
|
|
78
|
-
) {
|
|
79
|
-
const skillName = extractSkillName(block as ToolUseBlock);
|
|
80
|
-
if (skillName) {
|
|
81
|
-
results.push({ skillName, timestamp, sessionId });
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return results;
|
|
88
|
-
}
|
|
3
|
+
import { scanClaudeSessions, countClaudeSessions } from "./connectors/claude";
|
|
4
|
+
import {
|
|
5
|
+
scanOpenCodeSessions,
|
|
6
|
+
countOpenCodeSessions,
|
|
7
|
+
} from "./connectors/opencode";
|
|
89
8
|
|
|
90
9
|
interface AlreadyTracked {
|
|
91
10
|
session_id: string;
|
|
92
11
|
timestamp: string;
|
|
93
12
|
}
|
|
94
13
|
|
|
95
|
-
export
|
|
96
|
-
const projectsDir = join(homedir(), ".claude", "projects");
|
|
97
|
-
if (!existsSync(projectsDir)) return 0;
|
|
98
|
-
|
|
99
|
-
const glob = new Bun.Glob("**/*.jsonl");
|
|
100
|
-
const files: string[] = [];
|
|
101
|
-
|
|
102
|
-
for await (const file of glob.scan({ cwd: projectsDir, absolute: true })) {
|
|
103
|
-
files.push(file);
|
|
104
|
-
}
|
|
105
|
-
|
|
14
|
+
export function getTrackedSet(db: Database): Set<string> {
|
|
106
15
|
const tracked = db
|
|
107
16
|
.query<AlreadyTracked, []>(
|
|
108
17
|
"SELECT session_id, timestamp FROM skill_invocations WHERE session_id IS NOT NULL",
|
|
109
18
|
)
|
|
110
19
|
.all();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
20
|
+
return new Set(tracked.map((r) => `${r.session_id}::${r.timestamp}`));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Invocation {
|
|
24
|
+
skillName: string;
|
|
25
|
+
timestamp: string;
|
|
26
|
+
sessionId: string;
|
|
27
|
+
}
|
|
114
28
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
29
|
+
export function recordNewInvocations(
|
|
30
|
+
db: Database,
|
|
31
|
+
trackedSet: Set<string>,
|
|
32
|
+
invocations: Invocation[],
|
|
33
|
+
): number {
|
|
34
|
+
let count = 0;
|
|
35
|
+
for (const inv of invocations) {
|
|
36
|
+
const key = `${inv.sessionId}::${inv.timestamp}`;
|
|
37
|
+
if (!trackedSet.has(key)) {
|
|
38
|
+
recordInvocation(db, inv.skillName, inv.sessionId);
|
|
39
|
+
trackedSet.add(key);
|
|
40
|
+
count++;
|
|
125
41
|
}
|
|
126
42
|
}
|
|
43
|
+
return count;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function scanAllSessions(db: Database): Promise<number> {
|
|
47
|
+
const trackedSet = getTrackedSet(db);
|
|
48
|
+
let total = 0;
|
|
49
|
+
total += await scanClaudeSessions(db, trackedSet);
|
|
50
|
+
total += scanOpenCodeSessions(db, trackedSet);
|
|
51
|
+
return total;
|
|
52
|
+
}
|
|
127
53
|
|
|
128
|
-
|
|
54
|
+
export function countAllSessions(): number {
|
|
55
|
+
return countClaudeSessions() + countOpenCodeSessions();
|
|
129
56
|
}
|
package/src/scanner/skills.ts
CHANGED
|
@@ -3,6 +3,24 @@ 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 }> = [
|
|
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
|
+
{ 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") },
|
|
22
|
+
];
|
|
23
|
+
|
|
6
24
|
function parseYamlFrontmatter(content: string): Record<string, string> {
|
|
7
25
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
8
26
|
if (!match || !match[1]) return {};
|
|
@@ -38,12 +56,13 @@ function getDirSize(dirPath: string): number {
|
|
|
38
56
|
return total;
|
|
39
57
|
}
|
|
40
58
|
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
function scanSkillsDir(
|
|
60
|
+
skillsDir: string,
|
|
61
|
+
agent: string,
|
|
62
|
+
): InstalledSkill[] {
|
|
43
63
|
if (!existsSync(skillsDir)) return [];
|
|
44
64
|
|
|
45
65
|
const skills: InstalledSkill[] = [];
|
|
46
|
-
|
|
47
66
|
let entries: string[];
|
|
48
67
|
try {
|
|
49
68
|
entries = readdirSync(skillsDir);
|
|
@@ -95,8 +114,37 @@ export function scanInstalledSkills(): InstalledSkill[] {
|
|
|
95
114
|
description,
|
|
96
115
|
size,
|
|
97
116
|
installedAt: new Date(stat.birthtime).toISOString(),
|
|
117
|
+
agent,
|
|
98
118
|
});
|
|
99
119
|
}
|
|
100
120
|
|
|
101
|
-
return skills
|
|
121
|
+
return skills;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function scanInstalledSkills(): InstalledSkill[] {
|
|
125
|
+
const allSkills: InstalledSkill[] = [];
|
|
126
|
+
const seen = new Set<string>();
|
|
127
|
+
|
|
128
|
+
for (const { agent, dir } of AGENT_SKILL_PATHS) {
|
|
129
|
+
const skills = scanSkillsDir(dir, agent);
|
|
130
|
+
for (const skill of skills) {
|
|
131
|
+
try {
|
|
132
|
+
const ino = statSync(skill.path).ino;
|
|
133
|
+
const key = `${ino}`;
|
|
134
|
+
if (seen.has(key)) continue;
|
|
135
|
+
seen.add(key);
|
|
136
|
+
} catch {}
|
|
137
|
+
allSkills.push(skill);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return allSkills.sort((a, b) => a.name.localeCompare(b.name));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getDetectedAgents(): string[] {
|
|
145
|
+
const agents: string[] = [];
|
|
146
|
+
for (const { agent, dir } of AGENT_SKILL_PATHS) {
|
|
147
|
+
if (existsSync(dir)) agents.push(agent);
|
|
148
|
+
}
|
|
149
|
+
return agents;
|
|
102
150
|
}
|