@skills-hub-ai/mcp 0.1.7 → 0.1.9
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/dist/api.d.ts +7 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +26 -3
- package/dist/api.js.map +1 -1
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +51 -54
- package/dist/installer.js.map +1 -1
- package/package.json +22 -10
- package/src/api.ts +36 -4
- package/src/index.test.ts +29 -25
- package/src/installer.test.ts +8 -1
- package/src/installer.ts +73 -55
package/dist/api.d.ts
CHANGED
|
@@ -43,4 +43,11 @@ export interface SkillDetailResult {
|
|
|
43
43
|
}
|
|
44
44
|
export declare function searchSkills(query: string, category?: string, limit?: number, org?: string): Promise<SkillSearchResult[]>;
|
|
45
45
|
export declare function getSkillDetail(slug: string): Promise<SkillDetailResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Record a completed install so it counts toward the skill's installCount and
|
|
48
|
+
* shows up as an MCP install in analytics. Best-effort: a tracking failure must
|
|
49
|
+
* never break an install that already wrote files locally, so this swallows all
|
|
50
|
+
* errors. Anonymous (no API token) installs are deduped server-side by IP.
|
|
51
|
+
*/
|
|
52
|
+
export declare function recordInstall(slug: string, target: "claude-code" | "cursor"): Promise<void>;
|
|
46
53
|
//# sourceMappingURL=api.d.ts.map
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE;QACX,QAAQ,EAAE,KAAK,CAAC;YACd,KAAK,EAAE;gBAAE,IAAI,EAAE,MAAM,CAAC;gBAAC,IAAI,EAAE,MAAM,CAAC;gBAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;aAAE,CAAC;YACnE,SAAS,EAAE,MAAM,CAAC;YAClB,UAAU,EAAE,OAAO,CAAC;SACrB,CAAC,CAAC;KACJ,GAAG,IAAI,CAAC;CACV;AAED,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,QAAQ,CAAC,EAAE,MAAM,EACjB,KAAK,SAAK,EACV,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAgB9B;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAS7E;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,GAAG,QAAQ,GAC/B,OAAO,CAAC,IAAI,CAAC,CAcf"}
|
package/dist/api.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
const API_URL = process.env.SKILLS_HUB_API_URL || "https://api.skills-hub.ai";
|
|
2
2
|
const API_TOKEN = process.env.SKILLS_HUB_API_TOKEN || "";
|
|
3
3
|
const TIMEOUT_MS = 30_000;
|
|
4
|
-
function fetchWithTimeout(url) {
|
|
4
|
+
function fetchWithTimeout(url, init = {}) {
|
|
5
5
|
const controller = new AbortController();
|
|
6
6
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
7
|
-
const headers = {
|
|
7
|
+
const headers = {
|
|
8
|
+
// Identifies MCP traffic to the API's analytics (web vs cli vs mcp).
|
|
9
|
+
"X-Skills-Hub-Client": "mcp",
|
|
10
|
+
...(init.headers ?? {}),
|
|
11
|
+
};
|
|
8
12
|
if (API_TOKEN) {
|
|
9
13
|
headers["Authorization"] = `ApiKey ${API_TOKEN}`;
|
|
10
14
|
}
|
|
11
|
-
return fetch(url, { signal: controller.signal, headers }).finally(() => clearTimeout(timer));
|
|
15
|
+
return fetch(url, { ...init, signal: controller.signal, headers }).finally(() => clearTimeout(timer));
|
|
12
16
|
}
|
|
13
17
|
export async function searchSkills(query, category, limit = 10, org) {
|
|
14
18
|
const params = new URLSearchParams({
|
|
@@ -34,4 +38,23 @@ export async function getSkillDetail(slug) {
|
|
|
34
38
|
}
|
|
35
39
|
return (await res.json());
|
|
36
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Record a completed install so it counts toward the skill's installCount and
|
|
43
|
+
* shows up as an MCP install in analytics. Best-effort: a tracking failure must
|
|
44
|
+
* never break an install that already wrote files locally, so this swallows all
|
|
45
|
+
* errors. Anonymous (no API token) installs are deduped server-side by IP.
|
|
46
|
+
*/
|
|
47
|
+
export async function recordInstall(slug, target) {
|
|
48
|
+
const platform = target === "cursor" ? "CURSOR" : "CLAUDE_CODE";
|
|
49
|
+
try {
|
|
50
|
+
await fetchWithTimeout(`${API_URL}/api/v1/skills/${encodeURIComponent(slug)}/install`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "Content-Type": "application/json" },
|
|
53
|
+
body: JSON.stringify({ platform }),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Swallow — install already succeeded locally; tracking is best-effort.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
37
60
|
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,2BAA2B,CAAC;AAC9E,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,EAAE,CAAC;AACzD,MAAM,UAAU,GAAG,MAAM,CAAC;AAE1B,SAAS,gBAAgB,
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/installer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAoDA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,wEAAwE;AACxE,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,aAAa,GAAG,QAAwB,GAC/C,OAAO,CAAC,aAAa,CAAC,CA4CxB"}
|
package/dist/installer.js
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
if (!slug || /[/\\]|\.\./.test(slug)) {
|
|
7
|
-
throw new Error(`Invalid slug: ${slug}`);
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
function yamlEscape(value) {
|
|
11
|
-
if (!value)
|
|
12
|
-
return value ?? "";
|
|
13
|
-
if (/[:\-#{}\[\]&*!|>'"%@`]/.test(value) || value.includes("\n")) {
|
|
14
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
15
|
-
}
|
|
16
|
-
return value;
|
|
17
|
-
}
|
|
4
|
+
import { buildSkillMarkdown, validateSlug, writeSkillFiles, } from "@skills-hub-ai/installer";
|
|
5
|
+
import { getSkillDetail, recordInstall, } from "./api.js";
|
|
18
6
|
function getSkillsDir(target) {
|
|
19
7
|
const home = homedir();
|
|
20
8
|
return target === "cursor"
|
|
@@ -27,40 +15,53 @@ function getCommandsDir(target) {
|
|
|
27
15
|
? join(home, ".cursor", "skills")
|
|
28
16
|
: join(home, ".claude", "commands");
|
|
29
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Build the slash command body for claude-code. The format differs from
|
|
20
|
+
* SKILL.md: no `---` delimiters, just frontmatter style fields followed
|
|
21
|
+
* by the body. Kept inline here because it is claude-code specific and
|
|
22
|
+
* not part of the shared installer API.
|
|
23
|
+
*/
|
|
24
|
+
function buildClaudeCommandContent(skill) {
|
|
25
|
+
return [
|
|
26
|
+
`name: ${skill.name}`,
|
|
27
|
+
`description: ${skill.description}`,
|
|
28
|
+
`version: ${skill.latestVersion}`,
|
|
29
|
+
`category: ${skill.category.slug}`,
|
|
30
|
+
"",
|
|
31
|
+
skill.instructions,
|
|
32
|
+
"",
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
30
35
|
/** Install a single skill from the API to the local skills directory */
|
|
31
36
|
export async function installSkill(slug, target = "claude-code") {
|
|
32
37
|
validateSlug(slug);
|
|
33
38
|
const skill = await getSkillDetail(slug);
|
|
34
39
|
const skillDir = join(getSkillsDir(target), slug);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
${skill.instructions}
|
|
44
|
-
`;
|
|
45
|
-
writeFileSync(join(skillDir, "SKILL.md"), content);
|
|
40
|
+
const skillMd = buildSkillMarkdown({
|
|
41
|
+
name: skill.name,
|
|
42
|
+
description: skill.description,
|
|
43
|
+
version: skill.latestVersion,
|
|
44
|
+
category: skill.category.slug,
|
|
45
|
+
}, skill.instructions);
|
|
46
|
+
writeSkillFiles(skillDir, [{ relativePath: "SKILL.md", content: skillMd }]);
|
|
46
47
|
// Register as slash command (Claude Code reads from ~/.claude/commands/)
|
|
47
48
|
if (target === "claude-code") {
|
|
48
49
|
const commandsDir = getCommandsDir(target);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
${skill.instructions}
|
|
56
|
-
`;
|
|
57
|
-
writeFileSync(join(commandsDir, `${slug}.md`), commandContent);
|
|
50
|
+
writeSkillFiles(commandsDir, [
|
|
51
|
+
{
|
|
52
|
+
relativePath: `${slug}.md`,
|
|
53
|
+
content: buildClaudeCommandContent(skill),
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
58
56
|
}
|
|
59
57
|
// Install composition dependencies
|
|
60
58
|
const deps = [];
|
|
61
59
|
if (skill.composition?.children.length) {
|
|
62
60
|
await installDependencies(skill, target, deps);
|
|
63
61
|
}
|
|
62
|
+
// Record the install so it counts toward installCount and shows up as an MCP
|
|
63
|
+
// install in analytics. Best-effort — never blocks a successful local write.
|
|
64
|
+
await recordInstall(slug, target);
|
|
64
65
|
return {
|
|
65
66
|
slug,
|
|
66
67
|
version: skill.latestVersion,
|
|
@@ -90,28 +91,24 @@ async function installDependencies(skill, target, installed, visited = new Set()
|
|
|
90
91
|
try {
|
|
91
92
|
const childDetail = await getSkillDetail(childSlug);
|
|
92
93
|
const childDir = join(skillsDir, childSlug);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
`;
|
|
103
|
-
writeFileSync(join(childDir, "SKILL.md"), content);
|
|
94
|
+
const childSkillMd = buildSkillMarkdown({
|
|
95
|
+
name: childDetail.name,
|
|
96
|
+
description: childDetail.description,
|
|
97
|
+
version: childDetail.latestVersion,
|
|
98
|
+
category: childDetail.category.slug,
|
|
99
|
+
}, childDetail.instructions);
|
|
100
|
+
writeSkillFiles(childDir, [
|
|
101
|
+
{ relativePath: "SKILL.md", content: childSkillMd },
|
|
102
|
+
]);
|
|
104
103
|
// Register child as slash command too
|
|
105
104
|
if (target === "claude-code") {
|
|
106
105
|
const commandsDir = getCommandsDir(target);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`;
|
|
114
|
-
writeFileSync(join(commandsDir, `${childSlug}.md`), commandContent);
|
|
106
|
+
writeSkillFiles(commandsDir, [
|
|
107
|
+
{
|
|
108
|
+
relativePath: `${childSlug}.md`,
|
|
109
|
+
content: buildClaudeCommandContent(childDetail),
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
115
112
|
}
|
|
116
113
|
installed.push(childSlug);
|
|
117
114
|
// Recurse if this child is also a composition
|
package/dist/installer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installer.js","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"installer.js","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skills-hub-ai/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "MCP server for skills-hub.ai — serve installed skills as prompts in any MCP-compatible AI tool",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mcp",
|
|
8
8
|
"model-context-protocol",
|
|
9
|
+
"mcp-server",
|
|
9
10
|
"claude-code",
|
|
11
|
+
"claude-desktop",
|
|
12
|
+
"claude-skills",
|
|
13
|
+
"agent-skills",
|
|
14
|
+
"skill-md",
|
|
15
|
+
"ai-skills",
|
|
10
16
|
"cursor",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"prompts"
|
|
17
|
+
"cline",
|
|
18
|
+
"continue",
|
|
19
|
+
"ai-prompts",
|
|
20
|
+
"developer-tools",
|
|
21
|
+
"anthropic",
|
|
22
|
+
"ai-search",
|
|
23
|
+
"skill-registry",
|
|
24
|
+
"skills-hub"
|
|
14
25
|
],
|
|
15
26
|
"repository": {
|
|
16
27
|
"type": "git",
|
|
@@ -27,14 +38,15 @@
|
|
|
27
38
|
"skills-hub-mcp": "dist/index.js"
|
|
28
39
|
},
|
|
29
40
|
"dependencies": {
|
|
30
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
31
|
-
"@skills-hub-ai/
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
|
+
"@skills-hub-ai/installer": "0.1.0",
|
|
43
|
+
"@skills-hub-ai/skill-parser": "0.2.3"
|
|
32
44
|
},
|
|
33
45
|
"devDependencies": {
|
|
34
|
-
"@types/node": "^25.
|
|
35
|
-
"tsx": "^4.
|
|
36
|
-
"typescript": "^
|
|
37
|
-
"vitest": "^
|
|
46
|
+
"@types/node": "^25.6.0",
|
|
47
|
+
"tsx": "^4.22.4",
|
|
48
|
+
"typescript": "^6.0.3",
|
|
49
|
+
"vitest": "^4.1.7"
|
|
38
50
|
},
|
|
39
51
|
"scripts": {
|
|
40
52
|
"dev": "tsx src/index.ts",
|
package/src/api.ts
CHANGED
|
@@ -2,15 +2,22 @@ const API_URL = process.env.SKILLS_HUB_API_URL || "https://api.skills-hub.ai";
|
|
|
2
2
|
const API_TOKEN = process.env.SKILLS_HUB_API_TOKEN || "";
|
|
3
3
|
const TIMEOUT_MS = 30_000;
|
|
4
4
|
|
|
5
|
-
function fetchWithTimeout(
|
|
5
|
+
function fetchWithTimeout(
|
|
6
|
+
url: string,
|
|
7
|
+
init: RequestInit = {},
|
|
8
|
+
): Promise<Response> {
|
|
6
9
|
const controller = new AbortController();
|
|
7
10
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
8
|
-
const headers: Record<string, string> = {
|
|
11
|
+
const headers: Record<string, string> = {
|
|
12
|
+
// Identifies MCP traffic to the API's analytics (web vs cli vs mcp).
|
|
13
|
+
"X-Skills-Hub-Client": "mcp",
|
|
14
|
+
...((init.headers as Record<string, string>) ?? {}),
|
|
15
|
+
};
|
|
9
16
|
if (API_TOKEN) {
|
|
10
17
|
headers["Authorization"] = `ApiKey ${API_TOKEN}`;
|
|
11
18
|
}
|
|
12
|
-
return fetch(url, { signal: controller.signal, headers }).finally(
|
|
13
|
-
clearTimeout(timer),
|
|
19
|
+
return fetch(url, { ...init, signal: controller.signal, headers }).finally(
|
|
20
|
+
() => clearTimeout(timer),
|
|
14
21
|
);
|
|
15
22
|
}
|
|
16
23
|
|
|
@@ -82,3 +89,28 @@ export async function getSkillDetail(slug: string): Promise<SkillDetailResult> {
|
|
|
82
89
|
|
|
83
90
|
return (await res.json()) as SkillDetailResult;
|
|
84
91
|
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Record a completed install so it counts toward the skill's installCount and
|
|
95
|
+
* shows up as an MCP install in analytics. Best-effort: a tracking failure must
|
|
96
|
+
* never break an install that already wrote files locally, so this swallows all
|
|
97
|
+
* errors. Anonymous (no API token) installs are deduped server-side by IP.
|
|
98
|
+
*/
|
|
99
|
+
export async function recordInstall(
|
|
100
|
+
slug: string,
|
|
101
|
+
target: "claude-code" | "cursor",
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const platform = target === "cursor" ? "CURSOR" : "CLAUDE_CODE";
|
|
104
|
+
try {
|
|
105
|
+
await fetchWithTimeout(
|
|
106
|
+
`${API_URL}/api/v1/skills/${encodeURIComponent(slug)}/install`,
|
|
107
|
+
{
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { "Content-Type": "application/json" },
|
|
110
|
+
body: JSON.stringify({ platform }),
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
} catch {
|
|
114
|
+
// Swallow — install already succeeded locally; tracking is best-effort.
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/index.test.ts
CHANGED
|
@@ -18,23 +18,25 @@ vi.mock("./installer.js", () => ({
|
|
|
18
18
|
vi.mock("@modelcontextprotocol/sdk/server/index.js", () => {
|
|
19
19
|
const handlers = new Map<string, (req: unknown) => Promise<unknown>>();
|
|
20
20
|
return {
|
|
21
|
-
Server: vi.fn().mockImplementation(()
|
|
22
|
-
|
|
23
|
-
(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
Server: vi.fn().mockImplementation(function () {
|
|
22
|
+
return {
|
|
23
|
+
setRequestHandler: vi.fn(
|
|
24
|
+
function (
|
|
25
|
+
schema: { method: string },
|
|
26
|
+
handler: (req: unknown) => Promise<unknown>,
|
|
27
|
+
) {
|
|
28
|
+
handlers.set(schema.method, handler);
|
|
29
|
+
},
|
|
30
|
+
),
|
|
31
|
+
connect: vi.fn(),
|
|
32
|
+
_handlers: handlers,
|
|
33
|
+
};
|
|
34
|
+
}),
|
|
33
35
|
};
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
vi.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({
|
|
37
|
-
StdioServerTransport: vi.fn(),
|
|
39
|
+
StdioServerTransport: vi.fn().mockImplementation(function () { return {}; }),
|
|
38
40
|
}));
|
|
39
41
|
|
|
40
42
|
vi.mock("@modelcontextprotocol/sdk/types.js", () => ({
|
|
@@ -110,22 +112,24 @@ beforeEach(async () => {
|
|
|
110
112
|
const h = new Map<string, (req: unknown) => Promise<unknown>>();
|
|
111
113
|
handlers = h;
|
|
112
114
|
return {
|
|
113
|
-
Server: vi.fn().mockImplementation(()
|
|
114
|
-
|
|
115
|
-
(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
115
|
+
Server: vi.fn().mockImplementation(function () {
|
|
116
|
+
return {
|
|
117
|
+
setRequestHandler: vi.fn(
|
|
118
|
+
function (
|
|
119
|
+
schema: { method: string },
|
|
120
|
+
handler: (req: unknown) => Promise<unknown>,
|
|
121
|
+
) {
|
|
122
|
+
h.set(schema.method, handler);
|
|
123
|
+
},
|
|
124
|
+
),
|
|
125
|
+
connect: vi.fn(),
|
|
126
|
+
};
|
|
127
|
+
}),
|
|
124
128
|
};
|
|
125
129
|
});
|
|
126
130
|
|
|
127
131
|
vi.doMock("@modelcontextprotocol/sdk/server/stdio.js", () => ({
|
|
128
|
-
StdioServerTransport: vi.fn(),
|
|
132
|
+
StdioServerTransport: vi.fn().mockImplementation(function () { return {}; }),
|
|
129
133
|
}));
|
|
130
134
|
|
|
131
135
|
vi.doMock("@modelcontextprotocol/sdk/types.js", () => ({
|
package/src/installer.test.ts
CHANGED
|
@@ -10,11 +10,13 @@ vi.mock("node:fs", () => ({
|
|
|
10
10
|
|
|
11
11
|
vi.mock("./api.js", () => ({
|
|
12
12
|
getSkillDetail: vi.fn(),
|
|
13
|
+
recordInstall: vi.fn(),
|
|
13
14
|
}));
|
|
14
15
|
|
|
15
|
-
import { getSkillDetail } from "./api.js";
|
|
16
|
+
import { getSkillDetail, recordInstall } from "./api.js";
|
|
16
17
|
|
|
17
18
|
const mockGetSkillDetail = vi.mocked(getSkillDetail);
|
|
19
|
+
const mockRecordInstall = vi.mocked(recordInstall);
|
|
18
20
|
const mockExistsSync = vi.mocked(existsSync);
|
|
19
21
|
const mockMkdirSync = vi.mocked(mkdirSync);
|
|
20
22
|
const mockWriteFileSync = vi.mocked(writeFileSync);
|
|
@@ -57,6 +59,11 @@ describe("installSkill", () => {
|
|
|
57
59
|
expect(result.slug).toBe("review-code");
|
|
58
60
|
expect(result.version).toBe("1.2.0");
|
|
59
61
|
expect(result.dependencies).toEqual([]);
|
|
62
|
+
// Records the install so it counts toward installCount + MCP analytics.
|
|
63
|
+
expect(mockRecordInstall).toHaveBeenCalledWith(
|
|
64
|
+
"review-code",
|
|
65
|
+
"claude-code",
|
|
66
|
+
);
|
|
60
67
|
});
|
|
61
68
|
|
|
62
69
|
it("writes SKILL.md and command file", async () => {
|
package/src/installer.ts
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (/[:\-#{}\[\]&*!|>'"%@`]/.test(value) || value.includes("\n")) {
|
|
15
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
16
|
-
}
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
4
|
+
import {
|
|
5
|
+
buildSkillMarkdown,
|
|
6
|
+
validateSlug,
|
|
7
|
+
writeSkillFiles,
|
|
8
|
+
} from "@skills-hub-ai/installer";
|
|
9
|
+
import {
|
|
10
|
+
getSkillDetail,
|
|
11
|
+
recordInstall,
|
|
12
|
+
type SkillDetailResult,
|
|
13
|
+
} from "./api.js";
|
|
19
14
|
|
|
20
15
|
function getSkillsDir(target: "claude-code" | "cursor"): string {
|
|
21
16
|
const home = homedir();
|
|
@@ -31,6 +26,30 @@ function getCommandsDir(target: "claude-code" | "cursor"): string {
|
|
|
31
26
|
: join(home, ".claude", "commands");
|
|
32
27
|
}
|
|
33
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Build the slash command body for claude-code. The format differs from
|
|
31
|
+
* SKILL.md: no `---` delimiters, just frontmatter style fields followed
|
|
32
|
+
* by the body. Kept inline here because it is claude-code specific and
|
|
33
|
+
* not part of the shared installer API.
|
|
34
|
+
*/
|
|
35
|
+
function buildClaudeCommandContent(skill: {
|
|
36
|
+
name: string;
|
|
37
|
+
description: string;
|
|
38
|
+
latestVersion: string;
|
|
39
|
+
category: { slug: string };
|
|
40
|
+
instructions: string;
|
|
41
|
+
}): string {
|
|
42
|
+
return [
|
|
43
|
+
`name: ${skill.name}`,
|
|
44
|
+
`description: ${skill.description}`,
|
|
45
|
+
`version: ${skill.latestVersion}`,
|
|
46
|
+
`category: ${skill.category.slug}`,
|
|
47
|
+
"",
|
|
48
|
+
skill.instructions,
|
|
49
|
+
"",
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
export interface InstallResult {
|
|
35
54
|
slug: string;
|
|
36
55
|
version: string;
|
|
@@ -47,32 +66,27 @@ export async function installSkill(
|
|
|
47
66
|
const skill = await getSkillDetail(slug);
|
|
48
67
|
const skillDir = join(getSkillsDir(target), slug);
|
|
49
68
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
const skillMd = buildSkillMarkdown(
|
|
70
|
+
{
|
|
71
|
+
name: skill.name,
|
|
72
|
+
description: skill.description,
|
|
73
|
+
version: skill.latestVersion,
|
|
74
|
+
category: skill.category.slug,
|
|
75
|
+
},
|
|
76
|
+
skill.instructions,
|
|
77
|
+
);
|
|
58
78
|
|
|
59
|
-
|
|
60
|
-
`;
|
|
61
|
-
|
|
62
|
-
writeFileSync(join(skillDir, "SKILL.md"), content);
|
|
79
|
+
writeSkillFiles(skillDir, [{ relativePath: "SKILL.md", content: skillMd }]);
|
|
63
80
|
|
|
64
81
|
// Register as slash command (Claude Code reads from ~/.claude/commands/)
|
|
65
82
|
if (target === "claude-code") {
|
|
66
83
|
const commandsDir = getCommandsDir(target);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
${skill.instructions}
|
|
74
|
-
`;
|
|
75
|
-
writeFileSync(join(commandsDir, `${slug}.md`), commandContent);
|
|
84
|
+
writeSkillFiles(commandsDir, [
|
|
85
|
+
{
|
|
86
|
+
relativePath: `${slug}.md`,
|
|
87
|
+
content: buildClaudeCommandContent(skill),
|
|
88
|
+
},
|
|
89
|
+
]);
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
// Install composition dependencies
|
|
@@ -81,6 +95,10 @@ ${skill.instructions}
|
|
|
81
95
|
await installDependencies(skill, target, deps);
|
|
82
96
|
}
|
|
83
97
|
|
|
98
|
+
// Record the install so it counts toward installCount and shows up as an MCP
|
|
99
|
+
// install in analytics. Best-effort — never blocks a successful local write.
|
|
100
|
+
await recordInstall(slug, target);
|
|
101
|
+
|
|
84
102
|
return {
|
|
85
103
|
slug,
|
|
86
104
|
version: skill.latestVersion,
|
|
@@ -118,30 +136,30 @@ async function installDependencies(
|
|
|
118
136
|
try {
|
|
119
137
|
const childDetail = await getSkillDetail(childSlug);
|
|
120
138
|
const childDir = join(skillsDir, childSlug);
|
|
121
|
-
mkdirSync(childDir, { recursive: true });
|
|
122
139
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
140
|
+
const childSkillMd = buildSkillMarkdown(
|
|
141
|
+
{
|
|
142
|
+
name: childDetail.name,
|
|
143
|
+
description: childDetail.description,
|
|
144
|
+
version: childDetail.latestVersion,
|
|
145
|
+
category: childDetail.category.slug,
|
|
146
|
+
},
|
|
147
|
+
childDetail.instructions,
|
|
148
|
+
);
|
|
129
149
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
150
|
+
writeSkillFiles(childDir, [
|
|
151
|
+
{ relativePath: "SKILL.md", content: childSkillMd },
|
|
152
|
+
]);
|
|
133
153
|
|
|
134
154
|
// Register child as slash command too
|
|
135
155
|
if (target === "claude-code") {
|
|
136
156
|
const commandsDir = getCommandsDir(target);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
`;
|
|
144
|
-
writeFileSync(join(commandsDir, `${childSlug}.md`), commandContent);
|
|
157
|
+
writeSkillFiles(commandsDir, [
|
|
158
|
+
{
|
|
159
|
+
relativePath: `${childSlug}.md`,
|
|
160
|
+
content: buildClaudeCommandContent(childDetail),
|
|
161
|
+
},
|
|
162
|
+
]);
|
|
145
163
|
}
|
|
146
164
|
|
|
147
165
|
installed.push(childSlug);
|