@skills-hub-ai/mcp 0.1.8 → 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 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":"AAgBA,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"}
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,CAAC,GAAW;IACnC,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,UAAU,CAAC,CAAC;IAC/D,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,SAAS,EAAE,CAAC;IACnD,CAAC;IACD,OAAO,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CACrE,YAAY,CAAC,KAAK,CAAC,CACpB,CAAC;AACJ,CAAC;AAqCD,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,KAAa,EACb,QAAiB,EACjB,KAAK,GAAG,EAAE,EACV,GAAY;IAEZ,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,CAAC,EAAE,KAAK;QACR,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC;QACpB,IAAI,EAAE,WAAW;KAClB,CAAC,CAAC;IACH,IAAI,QAAQ;QAAE,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAC/C,IAAI,GAAG;QAAE,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAEhC,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,GAAG,OAAO,kBAAkB,MAAM,EAAE,CAAC,CAAC;IACzE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,kBAAkB,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAmC,CAAC;IAClE,OAAO,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY;IAC/C,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAChC,GAAG,OAAO,kBAAkB,kBAAkB,CAAC,IAAI,CAAC,EAAE,CACvD,CAAC;IACF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,oBAAoB,IAAI,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;IAC9D,CAAC;IAED,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAsB,CAAC;AACjD,CAAC"}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAiCA,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,CA6CxB"}
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, mkdirSync, writeFileSync } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
- import { getSkillDetail } from "./api.js";
5
- function validateSlug(slug) {
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
- mkdirSync(skillDir, { recursive: true });
36
- const content = `---
37
- name: ${yamlEscape(skill.name)}
38
- description: ${yamlEscape(skill.description)}
39
- version: ${yamlEscape(skill.latestVersion)}
40
- category: ${yamlEscape(skill.category.slug)}
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
- mkdirSync(commandsDir, { recursive: true });
50
- const commandContent = `name: ${yamlEscape(skill.name)}
51
- description: ${yamlEscape(skill.description)}
52
- version: ${yamlEscape(skill.latestVersion)}
53
- category: ${yamlEscape(skill.category.slug)}
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
- mkdirSync(childDir, { recursive: true });
94
- const content = `---
95
- name: ${yamlEscape(childDetail.name)}
96
- description: ${yamlEscape(childDetail.description)}
97
- version: ${yamlEscape(childDetail.latestVersion)}
98
- category: ${yamlEscape(childDetail.category.slug)}
99
- ---
100
-
101
- ${childDetail.instructions}
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
- const commandContent = `name: ${yamlEscape(childDetail.name)}
108
- description: ${yamlEscape(childDetail.description)}
109
- version: ${yamlEscape(childDetail.latestVersion)}
110
- category: ${yamlEscape(childDetail.category.slug)}
111
-
112
- ${childDetail.instructions}
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
@@ -1 +1 @@
1
- {"version":3,"file":"installer.js","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"installer.js","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skills-hub-ai/mcp",
3
- "version": "0.1.8",
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": [
@@ -39,13 +39,14 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@modelcontextprotocol/sdk": "^1.29.0",
42
- "@skills-hub-ai/skill-parser": "0.2.2"
42
+ "@skills-hub-ai/installer": "0.1.0",
43
+ "@skills-hub-ai/skill-parser": "0.2.3"
43
44
  },
44
45
  "devDependencies": {
45
46
  "@types/node": "^25.6.0",
46
- "tsx": "^4.0.0",
47
+ "tsx": "^4.22.4",
47
48
  "typescript": "^6.0.3",
48
- "vitest": "^4.1.5"
49
+ "vitest": "^4.1.7"
49
50
  },
50
51
  "scripts": {
51
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(url: string): Promise<Response> {
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
+ }
@@ -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, mkdirSync, writeFileSync } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
- import { getSkillDetail, type SkillDetailResult } from "./api.js";
5
-
6
- function validateSlug(slug: string): void {
7
- if (!slug || /[/\\]|\.\./.test(slug)) {
8
- throw new Error(`Invalid slug: ${slug}`);
9
- }
10
- }
11
-
12
- function yamlEscape(value: string): string {
13
- if (!value) return value ?? "";
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
- mkdirSync(skillDir, { recursive: true });
51
-
52
- const content = `---
53
- name: ${yamlEscape(skill.name)}
54
- description: ${yamlEscape(skill.description)}
55
- version: ${yamlEscape(skill.latestVersion)}
56
- category: ${yamlEscape(skill.category.slug)}
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
- ${skill.instructions}
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
- mkdirSync(commandsDir, { recursive: true });
68
- const commandContent = `name: ${yamlEscape(skill.name)}
69
- description: ${yamlEscape(skill.description)}
70
- version: ${yamlEscape(skill.latestVersion)}
71
- category: ${yamlEscape(skill.category.slug)}
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 content = `---
124
- name: ${yamlEscape(childDetail.name)}
125
- description: ${yamlEscape(childDetail.description)}
126
- version: ${yamlEscape(childDetail.latestVersion)}
127
- category: ${yamlEscape(childDetail.category.slug)}
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
- ${childDetail.instructions}
131
- `;
132
- writeFileSync(join(childDir, "SKILL.md"), content);
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
- const commandContent = `name: ${yamlEscape(childDetail.name)}
138
- description: ${yamlEscape(childDetail.description)}
139
- version: ${yamlEscape(childDetail.latestVersion)}
140
- category: ${yamlEscape(childDetail.category.slug)}
141
-
142
- ${childDetail.instructions}
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);