skild 0.6.0 → 0.7.0

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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/dist/index.js +464 -46
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -32,6 +32,9 @@ skild uninstall pdf -t codex --local
32
32
 
33
33
  # Create a new Skill
34
34
  skild init my-skill
35
+
36
+ # Extract GitHub skills
37
+ skild extract-github-skills https://github.com/ComposioHQ/awesome-claude-skills
35
38
  ```
36
39
 
37
40
  Full user guide (CLI + registry + console):
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import chalk16 from "chalk";
5
+ import chalk17 from "chalk";
6
6
  import { createRequire } from "module";
7
7
 
8
8
  // src/commands/install.ts
@@ -71,10 +71,10 @@ var logger = {
71
71
  /**
72
72
  * Log a skill entry with status indicator.
73
73
  */
74
- skillEntry: (name, path5, hasSkillMd) => {
74
+ skillEntry: (name, path6, hasSkillMd) => {
75
75
  const status = hasSkillMd ? chalk.green("\u2713") : chalk.yellow("\u26A0");
76
76
  console.log(` ${status} ${chalk.cyan(name)}`);
77
- console.log(chalk.dim(` \u2514\u2500 ${path5}`));
77
+ console.log(chalk.dim(` \u2514\u2500 ${path6}`));
78
78
  },
79
79
  /**
80
80
  * Log installation result details.
@@ -337,6 +337,19 @@ function formatTreeNode(node, selection, isCursor, options = {}) {
337
337
  }
338
338
  return `${cursorMark}${indent}${checkbox} ${name}${count}${suffix}${hint}`;
339
339
  }
340
+ function truncateDescription(value, maxLen) {
341
+ const trimmed = value.trim();
342
+ if (trimmed.length <= maxLen) return trimmed;
343
+ return `${trimmed.slice(0, maxLen - 3).trimEnd()}...`;
344
+ }
345
+ function getSkillDescriptionSuffix(skills, node, isCursor) {
346
+ if (!isCursor || !node.isLeaf || node.leafIndices.length !== 1) return "";
347
+ const skill = skills[node.leafIndices[0]];
348
+ if (!skill?.description) return "";
349
+ const description = truncateDescription(skill.description, 72);
350
+ if (!description) return "";
351
+ return chalk2.dim(` - ${description}`);
352
+ }
340
353
  async function promptSkillsInteractive(skills, options = {}) {
341
354
  if (skills.length === 0) return null;
342
355
  const targetPlatforms = options.targetPlatforms || [];
@@ -355,19 +368,20 @@ async function promptSkillsInteractive(skills, options = {}) {
355
368
  subtitle: "\u2191\u2193 navigate \u2022 Space toggle \u2022 Enter confirm",
356
369
  buildTree: buildSkillTree,
357
370
  formatNode: (node, selection, isCursor) => {
358
- let suffix = "";
371
+ let installedSuffix = "";
359
372
  if (node.isLeaf && node.leafIndices.length === 1) {
360
373
  const skill = skills[node.leafIndices[0]];
361
374
  if (skill?.installedPlatforms?.length) {
362
375
  if (skill.installedPlatforms.length === targetPlatforms.length && targetPlatforms.length > 0) {
363
- suffix = chalk2.dim(" [installed]");
376
+ installedSuffix = chalk2.dim(" [installed]");
364
377
  } else if (skill.installedPlatforms.length > 0) {
365
- suffix = chalk2.dim(` [installed on ${skill.installedPlatforms.length}]`);
378
+ installedSuffix = chalk2.dim(` [installed on ${skill.installedPlatforms.length}]`);
366
379
  }
367
380
  }
368
381
  }
369
- const formatted = formatTreeNode(node, selection, isCursor, { suffix });
370
- if (isCursor && suffix && selection.state !== "all") {
382
+ const descriptionSuffix = getSkillDescriptionSuffix(skills, node, isCursor);
383
+ const formatted = formatTreeNode(node, selection, isCursor, { suffix: `${installedSuffix}${descriptionSuffix}` });
384
+ if (isCursor && installedSuffix && selection.state !== "all") {
371
385
  return formatted.replace("\u2190 Space to select", "\u2190 Space to reinstall");
372
386
  }
373
387
  return formatted;
@@ -388,7 +402,10 @@ async function promptSkillsTreeInteractive(skills, tree, options = {}) {
388
402
  title: "Select skills from markdown",
389
403
  subtitle: "\u2191\u2193 navigate \u2022 Space toggle \u2022 Enter confirm",
390
404
  buildTree: () => buildTreeFromSkillNodes(tree, skills.length),
391
- formatNode: (node, selection, isCursor) => formatTreeNode(node, selection, isCursor),
405
+ formatNode: (node, selection, isCursor) => {
406
+ const descriptionSuffix = getSkillDescriptionSuffix(skills, node, isCursor);
407
+ return formatTreeNode(node, selection, isCursor, { suffix: descriptionSuffix });
408
+ },
392
409
  defaultAll: options.defaultAll !== false
393
410
  });
394
411
  if (!selectedIndices) return null;
@@ -425,12 +442,32 @@ async function promptPlatformsInteractive(options = {}) {
425
442
  // src/commands/install-discovery.ts
426
443
  import fs from "fs";
427
444
  import path from "path";
445
+ import { parseSkillFrontmatter, readSkillMd } from "@skild/core";
428
446
  function parsePositiveInt(input, fallback) {
429
447
  if (input == null) return fallback;
430
448
  const n = typeof input === "number" ? input : Number(String(input).trim());
431
449
  if (!Number.isFinite(n) || n <= 0) return fallback;
432
450
  return Math.floor(n);
433
451
  }
452
+ function parseNonNegativeInt(input, fallback) {
453
+ if (input == null) return fallback;
454
+ const n = typeof input === "number" ? input : Number(String(input).trim());
455
+ if (!Number.isFinite(n) || n < 0) return fallback;
456
+ return Math.floor(n);
457
+ }
458
+ function extractSkillMetadata(skillMdContent) {
459
+ const frontmatter = parseSkillFrontmatter(skillMdContent);
460
+ if (!frontmatter) return null;
461
+ const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : void 0;
462
+ const description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : void 0;
463
+ if (!name && !description) return null;
464
+ return { name, description };
465
+ }
466
+ function readSkillMetadata(skillDir) {
467
+ const content = readSkillMd(skillDir);
468
+ if (!content) return null;
469
+ return extractSkillMetadata(content);
470
+ }
434
471
  function normalizeRelPath(relPath) {
435
472
  return relPath.split(path.sep).join("/").replace(/^\/+/, "").replace(/\/+$/, "");
436
473
  }
@@ -504,7 +541,7 @@ function discoverSkillDirsWithHeuristics(rootDir, options) {
504
541
  return discoverSkillDirs(root, options);
505
542
  }
506
543
 
507
- // src/commands/install-markdown.ts
544
+ // src/commands/markdown-discovery.ts
508
545
  import fs2 from "fs";
509
546
  import path2 from "path";
510
547
  import { unified } from "unified";
@@ -514,7 +551,8 @@ import { deriveChildSource, fetchWithTimeout, materializeSourceToTemp } from "@s
514
551
  var README_CANDIDATES = ["README.md", "readme.md", "Readme.md", "README.markdown", "readme.markdown"];
515
552
  async function discoverMarkdownSkillsFromSource(input) {
516
553
  const ctx = {
517
- maxDepth: input.maxDepth,
554
+ maxDocDepth: input.maxDocDepth,
555
+ maxSkillDepth: input.maxSkillDepth,
518
556
  maxSkills: input.maxSkills,
519
557
  maxLinks: Math.min(400, Math.max(200, input.maxSkills * 2)),
520
558
  linkLimitReached: false,
@@ -558,7 +596,7 @@ function cleanupRepoCache(ctx) {
558
596
  ctx.repoCache.clear();
559
597
  }
560
598
  async function parseMarkdownDoc(doc, ctx, depth, visitedDocs) {
561
- if (depth > ctx.maxDepth) return [];
599
+ if (depth > ctx.maxDocDepth) return [];
562
600
  const docKey = `${doc.repo.owner}/${doc.repo.repo}#${doc.repo.ref || ""}:${doc.docPath}`;
563
601
  if (visitedDocs.has(docKey)) return [];
564
602
  visitedDocs.add(docKey);
@@ -646,7 +684,8 @@ async function parseInlineLinks(node, parent, doc, ctx, depth, visitedDocs) {
646
684
  if (!resolved) continue;
647
685
  const label = normalizeLabel(link.label) || resolved.displayName;
648
686
  const maybeMarkdown = !isLikelyFilePath(resolved.pathHint) || looksLikeMarkdownPath(resolved.pathHint);
649
- if (maybeMarkdown) {
687
+ const canRecurseMarkdown = depth < ctx.maxDocDepth;
688
+ if (maybeMarkdown && canRecurseMarkdown) {
650
689
  const childDoc = await resolveMarkdownDoc(resolved.source, ctx);
651
690
  if (childDoc) {
652
691
  const childNodes = await parseMarkdownDoc(childDoc, ctx, depth + 1, visitedDocs);
@@ -666,10 +705,7 @@ async function parseInlineLinks(node, parent, doc, ctx, depth, visitedDocs) {
666
705
  parent.children.push(createSkillLeaf(ctx, skillIndices[0], label));
667
706
  } else {
668
707
  const groupLabel = label || resolved.displayName || "Skills";
669
- const groupNode = createNode(ctx, groupLabel, "list", []);
670
- for (const skillIndex of skillIndices) {
671
- groupNode.children.push(createSkillLeaf(ctx, skillIndex));
672
- }
708
+ const groupNode = buildSkillPathTree(ctx, skillIndices, groupLabel);
673
709
  parent.children.push(groupNode);
674
710
  }
675
711
  }
@@ -690,6 +726,50 @@ function createSkillLeaf(ctx, skillIndex, labelOverride) {
690
726
  node.skillIndex = skillIndex;
691
727
  return node;
692
728
  }
729
+ function buildSkillPathTree(ctx, skillIndices, label) {
730
+ const root = createNode(ctx, label || "Skills", "list", []);
731
+ const childMap = /* @__PURE__ */ new WeakMap();
732
+ const ensureChild = (parent, segment) => {
733
+ let map = childMap.get(parent);
734
+ if (!map) {
735
+ map = /* @__PURE__ */ new Map();
736
+ childMap.set(parent, map);
737
+ }
738
+ const existing = map.get(segment);
739
+ if (existing) return existing;
740
+ const node = createNode(ctx, segment, "list", []);
741
+ parent.children.push(node);
742
+ map.set(segment, node);
743
+ return node;
744
+ };
745
+ for (const skillIndex of skillIndices) {
746
+ const skill = ctx.skills[skillIndex];
747
+ if (!skill) continue;
748
+ let relPath = skill.relPath || "";
749
+ if (relPath === "." || relPath === "./") {
750
+ root.children.push(createSkillLeaf(ctx, skillIndex, skill.displayName));
751
+ continue;
752
+ }
753
+ if (relPath.startsWith("./")) relPath = relPath.slice(2);
754
+ relPath = relPath.replace(/^\/+/, "");
755
+ if (!relPath) {
756
+ root.children.push(createSkillLeaf(ctx, skillIndex, skill.displayName));
757
+ continue;
758
+ }
759
+ const segments = relPath.split("/").filter(Boolean);
760
+ if (segments.length === 0) {
761
+ root.children.push(createSkillLeaf(ctx, skillIndex, skill.displayName));
762
+ continue;
763
+ }
764
+ let parent = root;
765
+ for (let i = 0; i < segments.length - 1; i += 1) {
766
+ parent = ensureChild(parent, segments[i]);
767
+ }
768
+ const leafLabel = skill.displayName || segments[segments.length - 1];
769
+ parent.children.push(createSkillLeaf(ctx, skillIndex, leafLabel));
770
+ }
771
+ return root;
772
+ }
693
773
  function collapseSingleChildNodes(nodes) {
694
774
  const collapsed = [];
695
775
  for (const node of nodes) {
@@ -773,17 +853,19 @@ async function resolveSkillsFromSource(source, displayName, ctx, repoHint) {
773
853
  }
774
854
  const skillMd = path2.join(materializedDir, "SKILL.md");
775
855
  if (fs2.existsSync(skillMd)) {
856
+ const metadata = readSkillMetadata(materializedDir);
776
857
  const skillIndex = registerSkill(ctx, {
777
858
  relPath: ".",
778
859
  suggestedSource: source,
779
860
  materializedDir,
780
- displayName: displayName || deriveDisplayName(source)
861
+ displayName: metadata?.name || displayName || deriveDisplayName(source),
862
+ description: metadata?.description
781
863
  });
782
864
  const indices2 = [skillIndex];
783
865
  ctx.skillCache.set(source, indices2);
784
866
  return indices2;
785
867
  }
786
- const discovered = discoverSkillDirsWithHeuristics(materializedDir, { maxDepth: ctx.maxDepth, maxSkills: ctx.maxSkills });
868
+ const discovered = discoverSkillDirsWithHeuristics(materializedDir, { maxDepth: ctx.maxSkillDepth, maxSkills: ctx.maxSkills });
787
869
  if (discovered.length === 0) {
788
870
  ctx.skillCache.set(source, []);
789
871
  return [];
@@ -791,11 +873,13 @@ async function resolveSkillsFromSource(source, displayName, ctx, repoHint) {
791
873
  const indices = [];
792
874
  for (const skill of discovered) {
793
875
  const childSource = deriveChildSource(source, skill.relPath);
876
+ const metadata = readSkillMetadata(skill.absDir);
794
877
  const skillIndex = registerSkill(ctx, {
795
878
  relPath: skill.relPath,
796
879
  suggestedSource: childSource,
797
880
  materializedDir: skill.absDir,
798
- displayName: deriveDisplayName(childSource)
881
+ displayName: metadata?.name || deriveDisplayName(childSource),
882
+ description: metadata?.description
799
883
  });
800
884
  indices.push(skillIndex);
801
885
  }
@@ -808,23 +892,27 @@ function resolveSkillsFromLocal(repoHint, source, displayName, ctx) {
808
892
  if (!fs2.existsSync(base)) return [];
809
893
  const skillMd = path2.join(base, "SKILL.md");
810
894
  if (fs2.existsSync(skillMd)) {
895
+ const metadata = readSkillMetadata(base);
811
896
  return [
812
897
  registerSkill(ctx, {
813
898
  relPath: ".",
814
899
  suggestedSource: source,
815
- displayName: displayName || deriveDisplayName(source)
900
+ displayName: metadata?.name || displayName || deriveDisplayName(source),
901
+ description: metadata?.description
816
902
  })
817
903
  ];
818
904
  }
819
- const discovered = discoverSkillDirsWithHeuristics(base, { maxDepth: ctx.maxDepth, maxSkills: ctx.maxSkills });
905
+ const discovered = discoverSkillDirsWithHeuristics(base, { maxDepth: ctx.maxSkillDepth, maxSkills: ctx.maxSkills });
820
906
  if (discovered.length === 0) return [];
821
907
  const indices = [];
822
908
  for (const skill of discovered) {
823
909
  const childSource = deriveChildSource(source, skill.relPath);
910
+ const metadata = readSkillMetadata(skill.absDir);
824
911
  const skillIndex = registerSkill(ctx, {
825
912
  relPath: skill.relPath,
826
913
  suggestedSource: childSource,
827
- displayName: deriveDisplayName(childSource)
914
+ displayName: metadata?.name || deriveDisplayName(childSource),
915
+ description: metadata?.description
828
916
  });
829
917
  indices.push(skillIndex);
830
918
  }
@@ -842,16 +930,19 @@ async function tryFetchSkillManifest(source, displayName, ctx) {
842
930
  if (res.status === 404) return [];
843
931
  return [];
844
932
  }
933
+ const content = await res.text();
934
+ const metadata = extractSkillMetadata(content);
935
+ return [
936
+ registerSkill(ctx, {
937
+ relPath: ".",
938
+ suggestedSource: source,
939
+ displayName: metadata?.name || displayName || deriveDisplayName(source),
940
+ description: metadata?.description
941
+ })
942
+ ];
845
943
  } catch {
846
944
  return [];
847
945
  }
848
- return [
849
- registerSkill(ctx, {
850
- relPath: ".",
851
- suggestedSource: source,
852
- displayName: displayName || deriveDisplayName(source)
853
- })
854
- ];
855
946
  }
856
947
  function registerSkill(ctx, skill) {
857
948
  const key = skill.suggestedSource;
@@ -1046,12 +1137,18 @@ function getPlatformPromptList(scope) {
1046
1137
  const installed = getInstalledPlatforms(scope);
1047
1138
  return installed.length > 0 ? installed : [...PLATFORMS2];
1048
1139
  }
1049
- function asDiscoveredSkills(discovered, toSuggestedSource, toMaterializedDir) {
1050
- return discovered.map((d) => ({
1051
- relPath: d.relPath,
1052
- suggestedSource: toSuggestedSource(d),
1053
- materializedDir: toMaterializedDir ? toMaterializedDir(d) : void 0
1054
- }));
1140
+ function asDiscoveredSkills(discovered, toSuggestedSource, toMaterializedDir, toMetadataDir) {
1141
+ return discovered.map((d) => {
1142
+ const metadataDir = toMetadataDir ? toMetadataDir(d) : void 0;
1143
+ const metadata = metadataDir ? readSkillMetadata(metadataDir) : null;
1144
+ return {
1145
+ relPath: d.relPath,
1146
+ suggestedSource: toSuggestedSource(d),
1147
+ materializedDir: toMaterializedDir ? toMaterializedDir(d) : void 0,
1148
+ displayName: metadata?.name,
1149
+ description: metadata?.description
1150
+ };
1151
+ });
1055
1152
  }
1056
1153
  function createContext(source, options) {
1057
1154
  const scope = options.local ? "project" : "global";
@@ -1082,7 +1179,8 @@ function createContext(source, options) {
1082
1179
  jsonOnly,
1083
1180
  interactive,
1084
1181
  yes,
1085
- maxDepth: parsePositiveInt(options.depth, 6),
1182
+ markdownDepth: parseNonNegativeInt(options.depth, 0),
1183
+ scanDepth: parseNonNegativeInt(options.scanDepth, 6),
1086
1184
  maxSkills: parsePositiveInt(options.maxSkills, 200),
1087
1185
  resolvedSource: source.trim(),
1088
1186
  targets,
@@ -1129,7 +1227,7 @@ async function resolveSource(ctx) {
1129
1227
  return true;
1130
1228
  }
1131
1229
  async function discoverSkills(ctx) {
1132
- const { resolvedSource, maxDepth, maxSkills, jsonOnly } = ctx;
1230
+ const { resolvedSource, markdownDepth, scanDepth, maxSkills, jsonOnly } = ctx;
1133
1231
  if (ctx.spinner) {
1134
1232
  ctx.spinner.text = `Discovery at ${chalk3.cyan(ctx.source)}...`;
1135
1233
  }
@@ -1144,11 +1242,17 @@ async function discoverSkills(ctx) {
1144
1242
  if (isLocal) {
1145
1243
  const hasSkillMd2 = fs3.existsSync(path3.join(maybeLocalRoot, "SKILL.md"));
1146
1244
  if (hasSkillMd2) {
1245
+ const metadata = readSkillMetadata(maybeLocalRoot);
1147
1246
  ctx.isSingleSkill = true;
1148
- ctx.selectedSkills = [{ relPath: maybeLocalRoot, suggestedSource: resolvedSource }];
1247
+ ctx.selectedSkills = [{
1248
+ relPath: maybeLocalRoot,
1249
+ suggestedSource: resolvedSource,
1250
+ displayName: metadata?.name,
1251
+ description: metadata?.description
1252
+ }];
1149
1253
  return true;
1150
1254
  }
1151
- const discovered2 = discoverSkillDirsWithHeuristics(maybeLocalRoot, { maxDepth, maxSkills });
1255
+ const discovered2 = discoverSkillDirsWithHeuristics(maybeLocalRoot, { maxDepth: scanDepth, maxSkills });
1152
1256
  if (discovered2.length === 0) {
1153
1257
  const message = `No SKILL.md found at ${maybeLocalRoot} (or within subdirectories).`;
1154
1258
  if (jsonOnly) {
@@ -1160,12 +1264,18 @@ async function discoverSkills(ctx) {
1160
1264
  process.exitCode = 1;
1161
1265
  return false;
1162
1266
  }
1163
- ctx.discoveredSkills = asDiscoveredSkills(discovered2, (d) => path3.join(maybeLocalRoot, d.relPath));
1267
+ ctx.discoveredSkills = asDiscoveredSkills(
1268
+ discovered2,
1269
+ (d) => path3.join(maybeLocalRoot, d.relPath),
1270
+ void 0,
1271
+ (d) => d.absDir
1272
+ );
1164
1273
  return true;
1165
1274
  }
1166
1275
  const markdownResult = await discoverMarkdownSkillsFromSource({
1167
1276
  source: resolvedSource,
1168
- maxDepth,
1277
+ maxDocDepth: markdownDepth,
1278
+ maxSkillDepth: scanDepth,
1169
1279
  maxSkills,
1170
1280
  onProgress: (update2) => {
1171
1281
  if (ctx.spinner) {
@@ -1188,11 +1298,18 @@ async function discoverSkills(ctx) {
1188
1298
  ctx.cleanupMaterialized = appendCleanup(ctx.cleanupMaterialized, materialized.cleanup);
1189
1299
  const hasSkillMd = fs3.existsSync(path3.join(ctx.materializedDir, "SKILL.md"));
1190
1300
  if (hasSkillMd) {
1301
+ const metadata = readSkillMetadata(ctx.materializedDir);
1191
1302
  ctx.isSingleSkill = true;
1192
- ctx.selectedSkills = [{ relPath: ".", suggestedSource: resolvedSource, materializedDir: ctx.materializedDir }];
1303
+ ctx.selectedSkills = [{
1304
+ relPath: ".",
1305
+ suggestedSource: resolvedSource,
1306
+ materializedDir: ctx.materializedDir,
1307
+ displayName: metadata?.name,
1308
+ description: metadata?.description
1309
+ }];
1193
1310
  return true;
1194
1311
  }
1195
- const discovered = discoverSkillDirsWithHeuristics(ctx.materializedDir, { maxDepth, maxSkills });
1312
+ const discovered = discoverSkillDirsWithHeuristics(ctx.materializedDir, { maxDepth: scanDepth, maxSkills });
1196
1313
  if (discovered.length === 0) {
1197
1314
  const message = `No SKILL.md found in source "${resolvedSource}".`;
1198
1315
  if (jsonOnly) {
@@ -1207,6 +1324,7 @@ async function discoverSkills(ctx) {
1207
1324
  ctx.discoveredSkills = asDiscoveredSkills(
1208
1325
  discovered,
1209
1326
  (d) => deriveChildSource2(resolvedSource, d.relPath),
1327
+ (d) => d.absDir,
1210
1328
  (d) => d.absDir
1211
1329
  );
1212
1330
  return true;
@@ -2250,13 +2368,310 @@ async function search(query, options = {}) {
2250
2368
  }
2251
2369
  }
2252
2370
 
2371
+ // src/commands/extract-github-skills.ts
2372
+ import fs5 from "fs";
2373
+ import path5 from "path";
2374
+ import os2 from "os";
2375
+ import chalk16 from "chalk";
2376
+ import {
2377
+ deriveChildSource as deriveChildSource3,
2378
+ materializeSourceToTemp as materializeSourceToTemp3,
2379
+ SkildError as SkildError11
2380
+ } from "@skild/core";
2381
+ async function extractGithubSkills(source, options) {
2382
+ const resolvedSource = source.trim();
2383
+ const jsonOnly = Boolean(options.json);
2384
+ if (!parseGitHubSource(resolvedSource)) {
2385
+ const message = `Only GitHub sources are supported for extract-github-skills: "${resolvedSource}"`;
2386
+ if (jsonOnly) {
2387
+ process.stdout.write(JSON.stringify({ ok: false, error: message }, null, 2) + "\n");
2388
+ } else {
2389
+ console.error(chalk16.red(message));
2390
+ }
2391
+ process.exitCode = 1;
2392
+ return;
2393
+ }
2394
+ const maxDocDepth = parseNonNegativeInt(options.depth, 0);
2395
+ const maxSkillDepth = parseNonNegativeInt(options.scanDepth, 6);
2396
+ const maxSkills = parsePositiveInt(options.maxSkills, 200);
2397
+ const outDir = resolveOutDir(options.out);
2398
+ const force = Boolean(options.force);
2399
+ try {
2400
+ prepareOutputDir(outDir, force);
2401
+ } catch (error) {
2402
+ const message = error instanceof Error ? error.message : String(error);
2403
+ if (jsonOnly) {
2404
+ process.stdout.write(JSON.stringify({ ok: false, error: message }, null, 2) + "\n");
2405
+ } else {
2406
+ console.error(chalk16.red(message));
2407
+ }
2408
+ process.exitCode = 1;
2409
+ return;
2410
+ }
2411
+ const spinner = createSpinner(`Parsing markdown at ${chalk16.cyan(resolvedSource)}...`);
2412
+ if (!jsonOnly) spinner.start();
2413
+ let skills = [];
2414
+ let tree = [];
2415
+ let cleanup = null;
2416
+ try {
2417
+ const markdownResult = await discoverMarkdownSkillsFromSource({
2418
+ source: resolvedSource,
2419
+ maxDocDepth,
2420
+ maxSkillDepth,
2421
+ maxSkills,
2422
+ onProgress: (update2) => {
2423
+ if (!spinner) return;
2424
+ const current = update2.current ? ` \xB7 ${update2.current}` : "";
2425
+ const capped = update2.linkLimitReached ? " \xB7 link cap reached" : "";
2426
+ spinner.text = `Parsing markdown (${update2.docsScanned} docs, ${update2.linksChecked} links, ${update2.skillsFound} skills)${current}${capped}`;
2427
+ }
2428
+ });
2429
+ if (markdownResult && markdownResult.skills.length > 0) {
2430
+ skills = markdownResult.skills;
2431
+ tree = markdownResult.tree;
2432
+ cleanup = markdownResult.cleanup;
2433
+ } else {
2434
+ if (!jsonOnly) spinner.text = `Scanning repository at ${chalk16.cyan(resolvedSource)}...`;
2435
+ const materialized = await materializeSourceToTemp3(resolvedSource);
2436
+ cleanup = materialized.cleanup;
2437
+ const discovered = discoverSkillDirsWithHeuristics(materialized.dir, { maxDepth: maxSkillDepth, maxSkills });
2438
+ if (discovered.length === 0) {
2439
+ throw new SkildError11("SKILL_NOT_FOUND", `No SKILL.md found in source "${resolvedSource}".`);
2440
+ }
2441
+ skills = discovered.map((d) => {
2442
+ const metadata = readSkillMetadata(d.absDir);
2443
+ return {
2444
+ relPath: d.relPath,
2445
+ suggestedSource: d.relPath === "." ? resolvedSource : deriveChildSource3(resolvedSource, d.relPath),
2446
+ materializedDir: d.absDir,
2447
+ displayName: metadata?.name,
2448
+ description: metadata?.description
2449
+ };
2450
+ });
2451
+ tree = buildTreeFromRelPaths(skills);
2452
+ }
2453
+ if (skills.length > maxSkills) {
2454
+ throw new SkildError11("INVALID_SOURCE", `Found more than ${maxSkills} skills. Increase --max-skills to proceed.`);
2455
+ }
2456
+ const exportPaths = buildExportPathMap(tree, skills);
2457
+ const repoCache = /* @__PURE__ */ new Map();
2458
+ for (let i = 0; i < skills.length; i++) {
2459
+ const skill = skills[i];
2460
+ const exportSegments = exportPaths.get(i);
2461
+ if (!exportSegments || exportSegments.length === 0) continue;
2462
+ const exportPath = path5.join(outDir, ...exportSegments);
2463
+ const localDir = await resolveSkillDirectory(skill, repoCache);
2464
+ copyDirectory(localDir, exportPath);
2465
+ const metadata = readSkillMetadata(localDir) || { name: skill.displayName, description: skill.description };
2466
+ const skillJson = {
2467
+ name: metadata?.name || skill.displayName || skill.relPath || void 0,
2468
+ description: metadata?.description || skill.description || void 0,
2469
+ source: skill.suggestedSource,
2470
+ relPath: skill.relPath,
2471
+ exportPath: path5.relative(outDir, exportPath).split(path5.sep).join("/")
2472
+ };
2473
+ fs5.writeFileSync(path5.join(exportPath, "skill.json"), JSON.stringify(skillJson, null, 2));
2474
+ }
2475
+ const catalogSkills = skills.map((skill, index) => {
2476
+ const exportSegments = exportPaths.get(index) || [];
2477
+ const exportPath = exportSegments.length > 0 ? exportSegments.join("/") : slugifySegment(skill.displayName || skill.relPath || "skill", "skill");
2478
+ return {
2479
+ index,
2480
+ name: skill.displayName || void 0,
2481
+ description: skill.description || void 0,
2482
+ source: skill.suggestedSource,
2483
+ relPath: skill.relPath,
2484
+ exportPath
2485
+ };
2486
+ });
2487
+ const catalog = {
2488
+ schemaVersion: 1,
2489
+ source: resolvedSource,
2490
+ exportRoot: outDir,
2491
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2492
+ tree,
2493
+ skills: catalogSkills
2494
+ };
2495
+ fs5.writeFileSync(path5.join(outDir, "catalog.json"), JSON.stringify(catalog, null, 2));
2496
+ for (const entry of repoCache.values()) entry.cleanup();
2497
+ cleanup?.();
2498
+ if (!jsonOnly) {
2499
+ spinner.succeed(`Exported ${skills.length} skill${skills.length > 1 ? "s" : ""} to ${chalk16.cyan(outDir)}`);
2500
+ } else {
2501
+ process.stdout.write(JSON.stringify({ ok: true, outDir, skills: catalogSkills }, null, 2) + "\n");
2502
+ }
2503
+ } catch (error) {
2504
+ cleanup?.();
2505
+ if (!jsonOnly) spinner.stop();
2506
+ const message = error instanceof SkildError11 ? error.message : error instanceof Error ? error.message : String(error);
2507
+ if (jsonOnly) {
2508
+ process.stdout.write(JSON.stringify({ ok: false, error: message }, null, 2) + "\n");
2509
+ } else {
2510
+ console.error(chalk16.red(message));
2511
+ }
2512
+ process.exitCode = 1;
2513
+ }
2514
+ }
2515
+ function resolveOutDir(out) {
2516
+ if (!out || !out.trim()) {
2517
+ return path5.resolve(process.cwd(), "skild-github-skills");
2518
+ }
2519
+ const trimmed = out.trim();
2520
+ if (trimmed.startsWith("~")) {
2521
+ return path5.resolve(os2.homedir(), trimmed.slice(1));
2522
+ }
2523
+ return path5.resolve(process.cwd(), trimmed);
2524
+ }
2525
+ function prepareOutputDir(outDir, force) {
2526
+ if (fs5.existsSync(outDir)) {
2527
+ const entries = fs5.readdirSync(outDir);
2528
+ if (entries.length > 0) {
2529
+ if (!force) {
2530
+ throw new Error(`Output directory is not empty: ${outDir}. Use --force to overwrite.`);
2531
+ }
2532
+ fs5.rmSync(outDir, { recursive: true, force: true });
2533
+ }
2534
+ }
2535
+ fs5.mkdirSync(outDir, { recursive: true });
2536
+ }
2537
+ async function resolveSkillDirectory(skill, repoCache) {
2538
+ if (skill.materializedDir && fs5.existsSync(skill.materializedDir)) return skill.materializedDir;
2539
+ const parsed = parseGitHubSource(skill.suggestedSource);
2540
+ if (!parsed) throw new SkildError11("INVALID_SOURCE", `Unsupported skill source: ${skill.suggestedSource}`);
2541
+ const ref = parsed.ref || "HEAD";
2542
+ const repoKey = `${parsed.owner}/${parsed.repo}#${ref}`;
2543
+ let cached = repoCache.get(repoKey);
2544
+ if (!cached) {
2545
+ const materialized = await materializeSourceToTemp3(repoKey);
2546
+ cached = { dir: materialized.dir, cleanup: materialized.cleanup };
2547
+ repoCache.set(repoKey, cached);
2548
+ }
2549
+ const relPath = parsed.path ? parsed.path.replace(/^\/+/, "") : "";
2550
+ const resolved = relPath ? path5.join(cached.dir, relPath) : cached.dir;
2551
+ if (!fs5.existsSync(resolved)) {
2552
+ throw new SkildError11("SKILL_NOT_FOUND", `Skill path missing in repo: ${skill.suggestedSource}`);
2553
+ }
2554
+ return resolved;
2555
+ }
2556
+ function copyDirectory(fromDir, toDir) {
2557
+ fs5.mkdirSync(path5.dirname(toDir), { recursive: true });
2558
+ fs5.cpSync(fromDir, toDir, { recursive: true, errorOnExist: false, force: true });
2559
+ }
2560
+ function buildTreeFromRelPaths(skills) {
2561
+ let nodeId = 0;
2562
+ const root = { id: "root", label: "root", kind: "list", children: [] };
2563
+ const ensureChild = (parent, label) => {
2564
+ const found = parent.children.find((child2) => child2.label === label);
2565
+ if (found) return found;
2566
+ nodeId += 1;
2567
+ const child = { id: `rel-${nodeId}`, label, kind: "list", children: [] };
2568
+ parent.children.push(child);
2569
+ return child;
2570
+ };
2571
+ skills.forEach((skill, index) => {
2572
+ const relPath = skill.relPath === "." ? "" : skill.relPath;
2573
+ const segments = relPath.split("/").filter(Boolean);
2574
+ let parent = root;
2575
+ if (segments.length === 0) {
2576
+ nodeId += 1;
2577
+ parent.children.push({
2578
+ id: `rel-${nodeId}`,
2579
+ label: skill.displayName || "skill",
2580
+ kind: "skill",
2581
+ skillIndex: index,
2582
+ children: []
2583
+ });
2584
+ return;
2585
+ }
2586
+ for (let i = 0; i < segments.length - 1; i += 1) {
2587
+ parent = ensureChild(parent, segments[i]);
2588
+ }
2589
+ nodeId += 1;
2590
+ parent.children.push({
2591
+ id: `rel-${nodeId}`,
2592
+ label: skill.displayName || segments[segments.length - 1],
2593
+ kind: "skill",
2594
+ skillIndex: index,
2595
+ children: []
2596
+ });
2597
+ });
2598
+ return collapseSingleChildNodes2(root.children);
2599
+ }
2600
+ function buildExportPathMap(tree, skills) {
2601
+ const paths = /* @__PURE__ */ new Map();
2602
+ const usedPaths = /* @__PURE__ */ new Set();
2603
+ const walk = (nodes, parentSegments) => {
2604
+ const siblingCounts = /* @__PURE__ */ new Map();
2605
+ for (const node of nodes) {
2606
+ const base = slugifySegment(node.label, node.kind === "skill" ? "skill" : "section");
2607
+ const segment = ensureUniqueSegment(base, siblingCounts);
2608
+ if (node.kind === "skill" && node.skillIndex != null) {
2609
+ const segments = ensureUniquePath([...parentSegments, segment], usedPaths);
2610
+ paths.set(node.skillIndex, segments);
2611
+ usedPaths.add(segments.join("/"));
2612
+ continue;
2613
+ }
2614
+ if (node.children.length > 0) {
2615
+ walk(node.children, [...parentSegments, segment]);
2616
+ }
2617
+ }
2618
+ };
2619
+ walk(tree, []);
2620
+ for (let i = 0; i < skills.length; i++) {
2621
+ if (paths.has(i)) continue;
2622
+ const skill = skills[i];
2623
+ const fallbackSegments = relPathSegments(skill);
2624
+ const segments = ensureUniquePath(fallbackSegments, usedPaths);
2625
+ paths.set(i, segments);
2626
+ usedPaths.add(segments.join("/"));
2627
+ }
2628
+ return paths;
2629
+ }
2630
+ function relPathSegments(skill) {
2631
+ if (skill.relPath && skill.relPath !== ".") {
2632
+ return skill.relPath.split("/").filter(Boolean).map((segment) => slugifySegment(segment, "skill"));
2633
+ }
2634
+ return [slugifySegment(skill.displayName || skill.relPath || "skill", "skill")];
2635
+ }
2636
+ function ensureUniqueSegment(base, counts) {
2637
+ const next = (counts.get(base) || 0) + 1;
2638
+ counts.set(base, next);
2639
+ return next === 1 ? base : `${base}-${next}`;
2640
+ }
2641
+ function ensureUniquePath(segments, usedPaths) {
2642
+ let candidate = segments.join("/");
2643
+ if (!usedPaths.has(candidate)) return segments;
2644
+ let suffix = 2;
2645
+ const baseSegments = [...segments];
2646
+ while (usedPaths.has(candidate)) {
2647
+ candidate = [...baseSegments.slice(0, -1), `${baseSegments[baseSegments.length - 1]}-${suffix}`].join("/");
2648
+ suffix += 1;
2649
+ }
2650
+ return candidate.split("/");
2651
+ }
2652
+ function slugifySegment(label, fallback) {
2653
+ const normalized = label.trim().toLowerCase();
2654
+ const slug = normalized.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
2655
+ return slug || fallback;
2656
+ }
2657
+ function collapseSingleChildNodes2(nodes) {
2658
+ return nodes.map((node) => collapseNode2(node)).filter(Boolean);
2659
+ }
2660
+ function collapseNode2(node) {
2661
+ node.children = node.children.map((child) => collapseNode2(child)).filter(Boolean);
2662
+ if (node.kind !== "heading" && node.kind !== "skill" && node.children.length === 1 && !node.skillIndex) {
2663
+ return node.children[0];
2664
+ }
2665
+ return node;
2666
+ }
2667
+
2253
2668
  // src/index.ts
2254
2669
  import { PLATFORMS as PLATFORMS4 } from "@skild/core";
2255
2670
  var require2 = createRequire(import.meta.url);
2256
2671
  var { version } = require2("../package.json");
2257
2672
  var program = new Command();
2258
2673
  program.name("skild").description("The npm for Agent Skills \u2014 Discover, install, manage, and publish AI Agent Skills with ease.").version(version);
2259
- program.command("install <source>").alias("i").description("Install a Skill from a Git URL, degit shorthand, or local directory").option("-t, --target <platform>", `Target platform: ${PLATFORMS4.join(", ")}`).option("--all", `Install to all platforms: ${PLATFORMS4.join(", ")}`).option("--recursive", "If source is a multi-skill directory/repo, install all discovered skills").option("-y, --yes", "Skip confirmation prompts (assume yes)").option("--depth <n>", "Max directory depth to scan for SKILL.md (default: 6)", "6").option("--max-skills <n>", "Max discovered skills to install (default: 200)", "200").option("-l, --local", "Install to project-level directory instead of global").option("-f, --force", "Overwrite existing installation").option("--registry <url>", "Registry base URL (default: https://registry.skild.sh)").option("--json", "Output JSON").action(async (source, options) => {
2674
+ program.command("install <source>").alias("i").description("Install a Skill from a Git URL, degit shorthand, or local directory").option("-t, --target <platform>", `Target platform: ${PLATFORMS4.join(", ")}`).option("--all", `Install to all platforms: ${PLATFORMS4.join(", ")}`).option("--recursive", "If source is a multi-skill directory/repo, install all discovered skills").option("-y, --yes", "Skip confirmation prompts (assume yes)").option("--depth <n>", "Max markdown recursion depth (default: 0)", "0").option("--scan-depth <n>", "Max directory depth to scan for SKILL.md (default: 6)", "6").option("--max-skills <n>", "Max discovered skills to install (default: 200)", "200").option("-l, --local", "Install to project-level directory instead of global").option("-f, --force", "Overwrite existing installation").option("--registry <url>", "Registry base URL (default: https://registry.skild.sh)").option("--json", "Output JSON").action(async (source, options) => {
2260
2675
  await install(source, options);
2261
2676
  });
2262
2677
  program.command("list").alias("ls").description("List installed Skills").option("-t, --target <platform>", `Target platform: ${PLATFORMS4.join(", ")} (optional; omit to list all)`).option("-l, --local", "List project-level directory instead of global").option("--paths", "Show install paths").option("--verbose", "Show skillset dependency details").option("--json", "Output JSON").action(async (options) => list(options));
@@ -2271,8 +2686,11 @@ program.command("logout").description("Remove stored registry credentials").acti
2271
2686
  program.command("whoami").description("Show current registry identity").action(async () => whoami());
2272
2687
  program.command("publish").description("Publish a Skill directory to the registry (hosted tarball)").option("--dir <path>", "Skill directory (defaults to cwd)").option("--name <@publisher/skill>", "Override skill name (defaults to SKILL.md frontmatter)").option("--skill-version <semver>", "Override version (defaults to SKILL.md frontmatter)").option("--alias <alias>", "Optional short identifier (global unique) for `skild install <alias>`").option("--description <text>", "Override description (defaults to SKILL.md frontmatter)").option("--targets <list>", "Comma-separated target platforms metadata (optional)").option("--tag <tag>", "Dist-tag (default: latest)", "latest").option("--registry <url>", "Registry base URL (defaults to saved login)").option("--json", "Output JSON").action(async (options) => publish(options));
2273
2688
  program.command("search <query>").description("Search Skills in the registry").option("--registry <url>", "Registry base URL (default: https://registry.skild.sh)").option("--limit <n>", "Max results (default: 50)", "50").option("--json", "Output JSON").action(async (query, options) => search(query, options));
2689
+ program.command("extract-github-skills <source>").description("Extract GitHub skills into a local catalog directory").option("--out <dir>", "Output directory (default: ./skild-github-skills)").option("-f, --force", "Overwrite existing output directory").option("--depth <n>", "Max markdown recursion depth (default: 0)", "0").option("--scan-depth <n>", "Max directory depth to scan for SKILL.md (default: 6)", "6").option("--max-skills <n>", "Max discovered skills to export (default: 200)", "200").option("--json", "Output JSON").action(async (source, options) => {
2690
+ await extractGithubSkills(source, options);
2691
+ });
2274
2692
  program.action(() => {
2275
- console.log(chalk16.bold("\n\u{1F6E1}\uFE0F skild \u2014 Get your agents skilled.\n"));
2693
+ console.log(chalk17.bold("\n\u{1F6E1}\uFE0F skild \u2014 Get your agents skilled.\n"));
2276
2694
  program.outputHelp();
2277
2695
  });
2278
2696
  var argv = process.argv.slice();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skild",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "The npm for Agent Skills — Discover, install, manage, and publish AI Agent Skills with ease.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -41,7 +41,7 @@
41
41
  "remark-parse": "^11.0.0",
42
42
  "tar": "^7.4.3",
43
43
  "unified": "^11.0.4",
44
- "@skild/core": "^0.5.2"
44
+ "@skild/core": "^0.7.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/mdast": "^4.0.4",