skillhub 0.1.16 → 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.
@@ -12,18 +12,29 @@ var PLATFORM_PATHS = {
12
12
  project: ".codex/skills"
13
13
  },
14
14
  copilot: {
15
- user: ".github/skills",
16
- project: ".github/skills"
15
+ user: ".github/instructions",
16
+ project: ".github/instructions"
17
17
  },
18
18
  cursor: {
19
- user: ".cursor/skills",
20
- project: ".cursor/skills"
19
+ user: ".cursor/rules",
20
+ project: ".cursor/rules"
21
21
  },
22
22
  windsurf: {
23
- user: ".windsurf/skills",
24
- project: ".windsurf/skills"
23
+ user: ".windsurf/rules",
24
+ project: ".windsurf/rules"
25
25
  }
26
26
  };
27
+ var FLAT_FILE_PLATFORMS = ["cursor", "windsurf", "copilot"];
28
+ function isFlatFilePlatform(platform) {
29
+ return FLAT_FILE_PLATFORMS.includes(platform);
30
+ }
31
+ function getPlatformFilePath(platform, skillName, fileName, project = false) {
32
+ const basePath = getSkillsPath(platform, project);
33
+ if (isFlatFilePlatform(platform)) {
34
+ return path.join(basePath, fileName);
35
+ }
36
+ return path.join(basePath, skillName, fileName);
37
+ }
27
38
  function getSkillsPath(platform, project = false) {
28
39
  const home = os.homedir();
29
40
  const cwd = process.cwd();
@@ -77,6 +88,8 @@ async function saveConfig(config) {
77
88
  }
78
89
 
79
90
  export {
91
+ isFlatFilePlatform,
92
+ getPlatformFilePath,
80
93
  getSkillsPath,
81
94
  getSkillPath,
82
95
  ensureSkillsDir,
package/dist/index.js CHANGED
@@ -2,12 +2,14 @@
2
2
  import {
3
3
  ensureSkillsDir,
4
4
  getConfigPath,
5
+ getPlatformFilePath,
5
6
  getSkillPath,
6
7
  getSkillsPath,
8
+ isFlatFilePlatform,
7
9
  isSkillInstalled,
8
10
  loadConfig,
9
11
  saveConfig
10
- } from "./chunk-YXEKBJKF.js";
12
+ } from "./chunk-SLCBZBS3.js";
11
13
 
12
14
  // src/index.ts
13
15
  import { Command } from "commander";
@@ -19,7 +21,7 @@ import fs from "fs-extra";
19
21
  import * as path from "path";
20
22
  import chalk from "chalk";
21
23
  import ora from "ora";
22
- import { parseSkillMd } from "skillhub-core";
24
+ import { parseSkillMd, parseGenericInstructionFile, INSTRUCTION_FILE_PATTERNS as INSTRUCTION_FILE_PATTERNS2 } from "skillhub-core";
23
25
 
24
26
  // src/utils/api.ts
25
27
  import https from "https";
@@ -154,6 +156,7 @@ async function getSkillFiles(id) {
154
156
 
155
157
  // src/utils/github.ts
156
158
  import { Octokit } from "@octokit/rest";
159
+ import { INSTRUCTION_FILE_PATTERNS } from "skillhub-core";
157
160
  import https2 from "https";
158
161
  var octokit = null;
159
162
  function getOctokit() {
@@ -191,17 +194,34 @@ async function fetchRawFile(owner, repo, path3, branch) {
191
194
  });
192
195
  });
193
196
  }
194
- async function fetchSkillContent(owner, repo, skillPath, branch = "main") {
197
+ function getInstructionFilename(sourceFormat) {
198
+ const pattern = INSTRUCTION_FILE_PATTERNS.find((p) => p.format === sourceFormat);
199
+ return pattern?.filename || "SKILL.md";
200
+ }
201
+ async function fetchSkillContent(owner, repo, skillPath, branch = "main", sourceFormat = "skill.md") {
195
202
  const client = getOctokit();
196
- const skillMdPath = skillPath ? `${skillPath}/SKILL.md` : "SKILL.md";
203
+ const filename = getInstructionFilename(sourceFormat);
204
+ const isStandalone = sourceFormat !== "skill.md";
205
+ const basePath = skillPath ? `${skillPath}/${filename}` : filename;
197
206
  let skillMdResponse;
198
- const pathsToTry = [
199
- skillMdPath,
200
- // Common skill directories
201
- ...skillPath && !skillPath.startsWith("skills/") ? [`skills/${skillPath}/SKILL.md`] : [],
202
- ...skillPath && !skillPath.startsWith(".claude/") ? [`.claude/skills/${skillPath}/SKILL.md`] : [],
203
- ...skillPath && !skillPath.startsWith(".github/") ? [`.github/skills/${skillPath}/SKILL.md`] : []
204
- ];
207
+ let pathsToTry;
208
+ if (sourceFormat === "cursorrules" || sourceFormat === "windsurfrules") {
209
+ pathsToTry = [filename];
210
+ } else if (sourceFormat === "copilot-instructions") {
211
+ pathsToTry = [`.github/${filename}`];
212
+ } else if (sourceFormat === "agents.md") {
213
+ pathsToTry = [basePath];
214
+ if (skillPath) {
215
+ pathsToTry.push(filename);
216
+ }
217
+ } else {
218
+ pathsToTry = [
219
+ basePath,
220
+ ...skillPath && !skillPath.startsWith("skills/") ? [`skills/${skillPath}/SKILL.md`] : [],
221
+ ...skillPath && !skillPath.startsWith(".claude/") ? [`.claude/skills/${skillPath}/SKILL.md`] : [],
222
+ ...skillPath && !skillPath.startsWith(".github/") ? [`.github/skills/${skillPath}/SKILL.md`] : []
223
+ ];
224
+ }
205
225
  for (const pathToTry of pathsToTry) {
206
226
  try {
207
227
  skillMdResponse = await client.repos.getContent({
@@ -233,12 +253,20 @@ async function fetchSkillContent(owner, repo, skillPath, branch = "main") {
233
253
  }
234
254
  }
235
255
  if (!skillMdResponse) {
236
- throw new Error(`SKILL.md not found at ${owner}/${repo} (tried ${pathsToTry.length} paths)`);
256
+ throw new Error(`${filename} not found at ${owner}/${repo} (tried ${pathsToTry.length} paths)`);
237
257
  }
238
258
  if (!("content" in skillMdResponse.data)) {
239
- throw new Error("SKILL.md not found");
259
+ throw new Error(`${filename} not found`);
240
260
  }
241
261
  const skillMd = Buffer.from(skillMdResponse.data.content, "base64").toString("utf-8");
262
+ if (isStandalone) {
263
+ return {
264
+ skillMd,
265
+ scripts: [],
266
+ references: [],
267
+ assets: []
268
+ };
269
+ }
242
270
  const scripts = [];
243
271
  try {
244
272
  const scriptsPath = skillPath ? `${skillPath}/scripts` : "scripts";
@@ -311,6 +339,108 @@ async function getDefaultBranch(owner, repo) {
311
339
  return response.data.default_branch;
312
340
  }
313
341
 
342
+ // src/utils/transform.ts
343
+ var WINDSURF_CHAR_LIMIT = 6e3;
344
+ var PLATFORM_FILE_CONFIGS = {
345
+ claude: {
346
+ getFileName: () => "SKILL.md",
347
+ keepOriginal: false,
348
+ transform: (raw) => ({ content: raw, warnings: [] })
349
+ },
350
+ codex: {
351
+ getFileName: () => "SKILL.md",
352
+ keepOriginal: false,
353
+ transform: (raw) => ({ content: raw, warnings: [] })
354
+ },
355
+ cursor: {
356
+ getFileName: (skillName) => `${skillName}.mdc`,
357
+ keepOriginal: true,
358
+ transform: transformForCursor
359
+ },
360
+ windsurf: {
361
+ getFileName: (skillName) => `${skillName}.md`,
362
+ keepOriginal: true,
363
+ transform: transformForWindsurf
364
+ },
365
+ copilot: {
366
+ getFileName: (skillName) => `${skillName}.instructions.md`,
367
+ keepOriginal: true,
368
+ transform: transformForCopilot
369
+ }
370
+ };
371
+ function getPlatformFileName(platform, skillName) {
372
+ return PLATFORM_FILE_CONFIGS[platform].getFileName(skillName);
373
+ }
374
+ function transformForPlatform(platform, rawSkillMd, parsed) {
375
+ return PLATFORM_FILE_CONFIGS[platform].transform(rawSkillMd, parsed);
376
+ }
377
+ function shouldKeepOriginal(platform) {
378
+ return PLATFORM_FILE_CONFIGS[platform].keepOriginal;
379
+ }
380
+ function transformForCursor(_raw, parsed) {
381
+ const warnings = [];
382
+ const mdcFields = [];
383
+ if (parsed.metadata.description) {
384
+ mdcFields.push(`description: ${parsed.metadata.description}`);
385
+ }
386
+ const filePatterns = parsed.metadata.triggers?.filePatterns;
387
+ if (filePatterns && filePatterns.length > 0) {
388
+ mdcFields.push(`globs: ${filePatterns.join(", ")}`);
389
+ mdcFields.push("alwaysApply: false");
390
+ } else {
391
+ mdcFields.push("alwaysApply: true");
392
+ }
393
+ const body = parsed.content.trim();
394
+ const mdcContent = `---
395
+ ${mdcFields.join("\n")}
396
+ ---
397
+ ${body}
398
+ `;
399
+ return { content: mdcContent, warnings };
400
+ }
401
+ function transformForWindsurf(_raw, parsed) {
402
+ const warnings = [];
403
+ let body = parsed.content.trim();
404
+ if (!body.startsWith("# ")) {
405
+ body = `# ${parsed.metadata.name}
406
+
407
+ ${body}`;
408
+ }
409
+ if (body.length > WINDSURF_CHAR_LIMIT) {
410
+ warnings.push(
411
+ `Content exceeds Windsurf's ${WINDSURF_CHAR_LIMIT} character limit (${body.length} chars). Truncating.`
412
+ );
413
+ body = truncateAtSectionBoundary(body, WINDSURF_CHAR_LIMIT);
414
+ }
415
+ return { content: body + "\n", warnings };
416
+ }
417
+ function transformForCopilot(_raw, parsed) {
418
+ let body = parsed.content.trim();
419
+ if (!body.startsWith("# ")) {
420
+ body = `# ${parsed.metadata.name}
421
+
422
+ ${body}`;
423
+ }
424
+ return { content: body + "\n", warnings: [] };
425
+ }
426
+ function truncateAtSectionBoundary(content, limit) {
427
+ const notice = "\n\n<!-- Truncated by SkillHub: see SKILL.md for full content -->\n";
428
+ const maxLen = limit - notice.length;
429
+ if (content.length <= maxLen) return content;
430
+ const truncated = content.slice(0, maxLen);
431
+ const lastHeading = truncated.lastIndexOf("\n## ");
432
+ const lastH1 = truncated.lastIndexOf("\n# ");
433
+ const cutPoint = Math.max(lastHeading, lastH1);
434
+ if (cutPoint > 0) {
435
+ return truncated.slice(0, cutPoint) + notice;
436
+ }
437
+ const lastParagraph = truncated.lastIndexOf("\n\n");
438
+ if (lastParagraph > 0) {
439
+ return truncated.slice(0, lastParagraph) + notice;
440
+ }
441
+ return truncated + notice;
442
+ }
443
+
314
444
  // src/commands/install.ts
315
445
  async function install(skillId, options) {
316
446
  const spinner = ora("Parsing skill ID...").start();
@@ -340,10 +470,12 @@ async function install(skillId, options) {
340
470
  }
341
471
  let skillName;
342
472
  let branch = "main";
473
+ let sourceFormat = "skill.md";
343
474
  if (skillInfo) {
344
475
  skillName = skillInfo.name;
345
476
  skillPath = skillInfo.skillPath;
346
477
  branch = skillInfo.branch || "main";
478
+ sourceFormat = skillInfo.sourceFormat || "skill.md";
347
479
  spinner.text = `Found skill: ${chalk.cyan(skillName)}`;
348
480
  } else {
349
481
  spinner.text = "Skill not in registry, fetching from GitHub...";
@@ -372,7 +504,10 @@ async function install(skillId, options) {
372
504
  spinner.text = "Downloading skill files...";
373
505
  const cachedFiles = await getSkillFiles(skillInfo.id);
374
506
  if (cachedFiles && cachedFiles.files.length > 0) {
375
- content = convertCachedFilesToSkillContent(cachedFiles);
507
+ if (cachedFiles.sourceFormat) {
508
+ sourceFormat = cachedFiles.sourceFormat;
509
+ }
510
+ content = convertCachedFilesToSkillContent(cachedFiles, sourceFormat);
376
511
  spinner.text = cachedFiles.fromCache ? `Using cached files (${cachedFiles.files.length} files)` : `Downloaded ${cachedFiles.files.length} files via API`;
377
512
  }
378
513
  } catch {
@@ -386,7 +521,7 @@ async function install(skillId, options) {
386
521
  spinner.text = `Downloading from GitHub: ${owner}/${repo}/${skillPath || ""}...`;
387
522
  }
388
523
  try {
389
- content = await fetchSkillContent(owner, repo, skillPath, branch);
524
+ content = await fetchSkillContent(owner, repo, skillPath, branch, sourceFormat);
390
525
  spinner.text = `Downloaded ${content.scripts.length} scripts, ${content.references.length} references`;
391
526
  } catch (error) {
392
527
  spinner.fail("Failed to download skill files");
@@ -408,7 +543,11 @@ async function install(skillId, options) {
408
543
  spinner.fail("Failed to download skill content");
409
544
  process.exit(1);
410
545
  }
411
- const parsed = parseSkillMd(content.skillMd);
546
+ const parsed = sourceFormat === "skill.md" ? parseSkillMd(content.skillMd) : parseGenericInstructionFile(content.skillMd, sourceFormat, {
547
+ name: skillName,
548
+ description: skillInfo?.description || null,
549
+ owner
550
+ });
412
551
  if (!parsed.validation.isValid) {
413
552
  spinner.warn("Skill has validation issues:");
414
553
  for (const error of parsed.validation.errors) {
@@ -444,13 +583,34 @@ A different skill is already installed with the name "${actualName}":`));
444
583
  }
445
584
  spinner.text = "Installing skill...";
446
585
  await fs.ensureDir(installPath);
447
- await fs.writeFile(path.join(installPath, "SKILL.md"), content.skillMd);
586
+ const platformFileName = getPlatformFileName(options.platform, actualName);
587
+ const { content: transformedContent, warnings: transformWarnings } = transformForPlatform(options.platform, content.skillMd, parsed);
588
+ for (const warning of transformWarnings) {
589
+ console.log(chalk.yellow(` Warning: ${warning}`));
590
+ }
591
+ if (isFlatFilePlatform(options.platform)) {
592
+ const platformFilePath2 = getPlatformFilePath(
593
+ options.platform,
594
+ actualName,
595
+ platformFileName,
596
+ options.project
597
+ );
598
+ await fs.writeFile(platformFilePath2, transformedContent);
599
+ } else {
600
+ await fs.writeFile(path.join(installPath, platformFileName), transformedContent);
601
+ }
602
+ if (shouldKeepOriginal(options.platform)) {
603
+ await fs.writeFile(path.join(installPath, "SKILL.md"), content.skillMd);
604
+ }
448
605
  const canonicalId = skillInfo?.id || skillId;
606
+ const platformFilePath = isFlatFilePlatform(options.platform) ? getPlatformFilePath(options.platform, actualName, platformFileName, options.project) : null;
449
607
  await fs.writeJson(path.join(installPath, ".skillhub.json"), {
450
608
  skillId: canonicalId,
451
609
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
452
610
  platform: options.platform,
453
- version: parsed.metadata.version || null
611
+ version: parsed.metadata.version || null,
612
+ platformFileName,
613
+ platformFilePath
454
614
  });
455
615
  if (content.scripts.length > 0) {
456
616
  const scriptsDir = path.join(installPath, "scripts");
@@ -511,30 +671,30 @@ function getPlatformName(platform) {
511
671
  function getPlatformSetupInstructions(platform, installPath) {
512
672
  switch (platform) {
513
673
  case "claude":
514
- return chalk.dim(" Skills in ~/.claude/skills/ are automatically discovered by Claude Code.");
674
+ return chalk.dim(" Skills in .claude/skills/ are automatically discovered by Claude Code.");
515
675
  case "codex":
516
676
  return chalk.dim(` Reference this skill in your AGENTS.md:
517
677
  @import ${installPath}/SKILL.md`);
518
678
  case "copilot":
519
- return chalk.dim(` Reference this skill in .github/copilot-instructions.md:
520
- @import ${installPath}/SKILL.md`);
679
+ return chalk.dim(" Instructions in .github/instructions/ are automatically loaded by GitHub Copilot.");
521
680
  case "cursor":
522
- return chalk.dim(` Reference this skill in your .cursorrules file:
523
- @import ${installPath}/SKILL.md`);
681
+ return chalk.dim(" Rules in .cursor/rules/ are automatically loaded by Cursor.");
524
682
  case "windsurf":
525
- return chalk.dim(` Reference this skill in your Windsurf configuration:
526
- Path: ${installPath}/SKILL.md`);
683
+ return chalk.dim(" Rules in .windsurf/rules/ are automatically loaded by Windsurf.");
527
684
  default:
528
685
  return null;
529
686
  }
530
687
  }
531
- function convertCachedFilesToSkillContent(response) {
688
+ var MAIN_FILE_NAMES = INSTRUCTION_FILE_PATTERNS2.map((p) => p.filename);
689
+ function convertCachedFilesToSkillContent(response, sourceFormat = "skill.md") {
532
690
  let skillMd = "";
533
691
  const scripts = [];
534
692
  const references = [];
693
+ const expectedPattern = INSTRUCTION_FILE_PATTERNS2.find((p) => p.format === sourceFormat);
694
+ const expectedFilename = expectedPattern?.filename || "SKILL.md";
535
695
  for (const file of response.files) {
536
696
  if (!file.content) continue;
537
- if (file.name === "SKILL.md" && (file.path === "SKILL.md" || file.path === file.name)) {
697
+ if (!skillMd && (file.name === expectedFilename || MAIN_FILE_NAMES.includes(file.name)) && file.path === file.name) {
538
698
  skillMd = file.content;
539
699
  continue;
540
700
  }
@@ -737,18 +897,31 @@ async function getInstalledSkills(platform, project = false) {
737
897
  }
738
898
  const skillPath = path2.join(skillsPath, entry.name);
739
899
  const skillMdPath = path2.join(skillPath, "SKILL.md");
740
- if (!await fs2.pathExists(skillMdPath)) {
900
+ const metadataPath = path2.join(skillPath, ".skillhub.json");
901
+ const hasSkillMd = await fs2.pathExists(skillMdPath);
902
+ const hasMetadata = await fs2.pathExists(metadataPath);
903
+ if (!hasSkillMd && !hasMetadata) {
741
904
  continue;
742
905
  }
743
906
  try {
744
- const content = await fs2.readFile(skillMdPath, "utf-8");
745
- const parsed = parseSkillMd2(content);
746
- skills.push({
747
- name: parsed.metadata.name || entry.name,
748
- description: parsed.metadata.description,
749
- version: parsed.metadata.version,
750
- path: skillPath
751
- });
907
+ if (hasSkillMd) {
908
+ const content = await fs2.readFile(skillMdPath, "utf-8");
909
+ const parsed = parseSkillMd2(content);
910
+ skills.push({
911
+ name: parsed.metadata.name || entry.name,
912
+ description: parsed.metadata.description,
913
+ version: parsed.metadata.version,
914
+ path: skillPath
915
+ });
916
+ } else if (hasMetadata) {
917
+ const metadata = await fs2.readJson(metadataPath);
918
+ skills.push({
919
+ name: entry.name,
920
+ description: void 0,
921
+ version: metadata.version || void 0,
922
+ path: skillPath
923
+ });
924
+ }
752
925
  } catch {
753
926
  skills.push({
754
927
  name: entry.name,
@@ -849,7 +1022,8 @@ program.command("config").description("Manage CLI configuration").option("--set
849
1022
  });
850
1023
  program.command("uninstall <skill-name>").description("Uninstall a skill").option("-p, --platform <platform>", "Target platform").option("--project", "Uninstall from project instead of globally").action(async (skillName, options) => {
851
1024
  const fs3 = await import("fs-extra");
852
- const { getSkillPath: getSkillPath2, isSkillInstalled: isSkillInstalled2 } = await import("./paths-BVI5WSHE.js");
1025
+ const pathModule = await import("path");
1026
+ const { getSkillPath: getSkillPath2, isSkillInstalled: isSkillInstalled2 } = await import("./paths-PDIRF66F.js");
853
1027
  const userConfig = await loadConfig();
854
1028
  const platform = options.platform || userConfig.defaultPlatform || "claude";
855
1029
  const installed = await isSkillInstalled2(platform, skillName, options.project);
@@ -858,13 +1032,23 @@ program.command("uninstall <skill-name>").description("Uninstall a skill").optio
858
1032
  process.exit(1);
859
1033
  }
860
1034
  const skillPath = getSkillPath2(platform, skillName, options.project);
1035
+ const metadataPath = pathModule.join(skillPath, ".skillhub.json");
1036
+ if (await fs3.default.pathExists(metadataPath)) {
1037
+ try {
1038
+ const metadata = await fs3.default.readJson(metadataPath);
1039
+ if (metadata.platformFilePath) {
1040
+ await fs3.default.remove(metadata.platformFilePath);
1041
+ }
1042
+ } catch {
1043
+ }
1044
+ }
861
1045
  await fs3.default.remove(skillPath);
862
1046
  console.log(chalk5.green(`Skill ${skillName} uninstalled successfully.`));
863
1047
  });
864
1048
  program.command("update [skill-name]").description("Update installed skills").option("-p, --platform <platform>", "Target platform").option("--all", "Update all installed skills").action(async (skillName, options) => {
865
1049
  const fsExtra = await import("fs-extra");
866
1050
  const pathModule = await import("path");
867
- const { getSkillsPath: getSkillsPath2, getSkillPath: getSkillPath2 } = await import("./paths-BVI5WSHE.js");
1051
+ const { getSkillsPath: getSkillsPath2, getSkillPath: getSkillPath2 } = await import("./paths-PDIRF66F.js");
868
1052
  const userConfig = await loadConfig();
869
1053
  const platform = options.platform || userConfig.defaultPlatform || "claude";
870
1054
  const ALL_PLATFORMS2 = ["claude", "codex", "copilot", "cursor", "windsurf"];
@@ -2,18 +2,22 @@ import {
2
2
  detectPlatform,
3
3
  ensureSkillsDir,
4
4
  getConfigPath,
5
+ getPlatformFilePath,
5
6
  getSkillPath,
6
7
  getSkillsPath,
8
+ isFlatFilePlatform,
7
9
  isSkillInstalled,
8
10
  loadConfig,
9
11
  saveConfig
10
- } from "./chunk-YXEKBJKF.js";
12
+ } from "./chunk-SLCBZBS3.js";
11
13
  export {
12
14
  detectPlatform,
13
15
  ensureSkillsDir,
14
16
  getConfigPath,
17
+ getPlatformFilePath,
15
18
  getSkillPath,
16
19
  getSkillsPath,
20
+ isFlatFilePlatform,
17
21
  isSkillInstalled,
18
22
  loadConfig,
19
23
  saveConfig
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillhub",
3
- "version": "0.1.16",
3
+ "version": "0.2.1",
4
4
  "description": "CLI tool for managing AI Agent skills - search, install, and update skills for Claude, Codex, Copilot, and more",
5
5
  "author": "SkillHub Contributors",
6
6
  "license": "MIT",