skild 0.6.1 → 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 +457 -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,6 +442,7 @@ 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());
@@ -437,6 +455,19 @@ function parseNonNegativeInt(input, fallback) {
437
455
  if (!Number.isFinite(n) || n < 0) return fallback;
438
456
  return Math.floor(n);
439
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
+ }
440
471
  function normalizeRelPath(relPath) {
441
472
  return relPath.split(path.sep).join("/").replace(/^\/+/, "").replace(/\/+$/, "");
442
473
  }
@@ -510,7 +541,7 @@ function discoverSkillDirsWithHeuristics(rootDir, options) {
510
541
  return discoverSkillDirs(root, options);
511
542
  }
512
543
 
513
- // src/commands/install-markdown.ts
544
+ // src/commands/markdown-discovery.ts
514
545
  import fs2 from "fs";
515
546
  import path2 from "path";
516
547
  import { unified } from "unified";
@@ -520,7 +551,8 @@ import { deriveChildSource, fetchWithTimeout, materializeSourceToTemp } from "@s
520
551
  var README_CANDIDATES = ["README.md", "readme.md", "Readme.md", "README.markdown", "readme.markdown"];
521
552
  async function discoverMarkdownSkillsFromSource(input) {
522
553
  const ctx = {
523
- maxDepth: input.maxDepth,
554
+ maxDocDepth: input.maxDocDepth,
555
+ maxSkillDepth: input.maxSkillDepth,
524
556
  maxSkills: input.maxSkills,
525
557
  maxLinks: Math.min(400, Math.max(200, input.maxSkills * 2)),
526
558
  linkLimitReached: false,
@@ -564,7 +596,7 @@ function cleanupRepoCache(ctx) {
564
596
  ctx.repoCache.clear();
565
597
  }
566
598
  async function parseMarkdownDoc(doc, ctx, depth, visitedDocs) {
567
- if (depth > ctx.maxDepth) return [];
599
+ if (depth > ctx.maxDocDepth) return [];
568
600
  const docKey = `${doc.repo.owner}/${doc.repo.repo}#${doc.repo.ref || ""}:${doc.docPath}`;
569
601
  if (visitedDocs.has(docKey)) return [];
570
602
  visitedDocs.add(docKey);
@@ -652,7 +684,7 @@ async function parseInlineLinks(node, parent, doc, ctx, depth, visitedDocs) {
652
684
  if (!resolved) continue;
653
685
  const label = normalizeLabel(link.label) || resolved.displayName;
654
686
  const maybeMarkdown = !isLikelyFilePath(resolved.pathHint) || looksLikeMarkdownPath(resolved.pathHint);
655
- const canRecurseMarkdown = depth < ctx.maxDepth;
687
+ const canRecurseMarkdown = depth < ctx.maxDocDepth;
656
688
  if (maybeMarkdown && canRecurseMarkdown) {
657
689
  const childDoc = await resolveMarkdownDoc(resolved.source, ctx);
658
690
  if (childDoc) {
@@ -673,10 +705,7 @@ async function parseInlineLinks(node, parent, doc, ctx, depth, visitedDocs) {
673
705
  parent.children.push(createSkillLeaf(ctx, skillIndices[0], label));
674
706
  } else {
675
707
  const groupLabel = label || resolved.displayName || "Skills";
676
- const groupNode = createNode(ctx, groupLabel, "list", []);
677
- for (const skillIndex of skillIndices) {
678
- groupNode.children.push(createSkillLeaf(ctx, skillIndex));
679
- }
708
+ const groupNode = buildSkillPathTree(ctx, skillIndices, groupLabel);
680
709
  parent.children.push(groupNode);
681
710
  }
682
711
  }
@@ -697,6 +726,50 @@ function createSkillLeaf(ctx, skillIndex, labelOverride) {
697
726
  node.skillIndex = skillIndex;
698
727
  return node;
699
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
+ }
700
773
  function collapseSingleChildNodes(nodes) {
701
774
  const collapsed = [];
702
775
  for (const node of nodes) {
@@ -780,17 +853,19 @@ async function resolveSkillsFromSource(source, displayName, ctx, repoHint) {
780
853
  }
781
854
  const skillMd = path2.join(materializedDir, "SKILL.md");
782
855
  if (fs2.existsSync(skillMd)) {
856
+ const metadata = readSkillMetadata(materializedDir);
783
857
  const skillIndex = registerSkill(ctx, {
784
858
  relPath: ".",
785
859
  suggestedSource: source,
786
860
  materializedDir,
787
- displayName: displayName || deriveDisplayName(source)
861
+ displayName: metadata?.name || displayName || deriveDisplayName(source),
862
+ description: metadata?.description
788
863
  });
789
864
  const indices2 = [skillIndex];
790
865
  ctx.skillCache.set(source, indices2);
791
866
  return indices2;
792
867
  }
793
- const discovered = discoverSkillDirsWithHeuristics(materializedDir, { maxDepth: ctx.maxDepth, maxSkills: ctx.maxSkills });
868
+ const discovered = discoverSkillDirsWithHeuristics(materializedDir, { maxDepth: ctx.maxSkillDepth, maxSkills: ctx.maxSkills });
794
869
  if (discovered.length === 0) {
795
870
  ctx.skillCache.set(source, []);
796
871
  return [];
@@ -798,11 +873,13 @@ async function resolveSkillsFromSource(source, displayName, ctx, repoHint) {
798
873
  const indices = [];
799
874
  for (const skill of discovered) {
800
875
  const childSource = deriveChildSource(source, skill.relPath);
876
+ const metadata = readSkillMetadata(skill.absDir);
801
877
  const skillIndex = registerSkill(ctx, {
802
878
  relPath: skill.relPath,
803
879
  suggestedSource: childSource,
804
880
  materializedDir: skill.absDir,
805
- displayName: deriveDisplayName(childSource)
881
+ displayName: metadata?.name || deriveDisplayName(childSource),
882
+ description: metadata?.description
806
883
  });
807
884
  indices.push(skillIndex);
808
885
  }
@@ -815,23 +892,27 @@ function resolveSkillsFromLocal(repoHint, source, displayName, ctx) {
815
892
  if (!fs2.existsSync(base)) return [];
816
893
  const skillMd = path2.join(base, "SKILL.md");
817
894
  if (fs2.existsSync(skillMd)) {
895
+ const metadata = readSkillMetadata(base);
818
896
  return [
819
897
  registerSkill(ctx, {
820
898
  relPath: ".",
821
899
  suggestedSource: source,
822
- displayName: displayName || deriveDisplayName(source)
900
+ displayName: metadata?.name || displayName || deriveDisplayName(source),
901
+ description: metadata?.description
823
902
  })
824
903
  ];
825
904
  }
826
- const discovered = discoverSkillDirsWithHeuristics(base, { maxDepth: ctx.maxDepth, maxSkills: ctx.maxSkills });
905
+ const discovered = discoverSkillDirsWithHeuristics(base, { maxDepth: ctx.maxSkillDepth, maxSkills: ctx.maxSkills });
827
906
  if (discovered.length === 0) return [];
828
907
  const indices = [];
829
908
  for (const skill of discovered) {
830
909
  const childSource = deriveChildSource(source, skill.relPath);
910
+ const metadata = readSkillMetadata(skill.absDir);
831
911
  const skillIndex = registerSkill(ctx, {
832
912
  relPath: skill.relPath,
833
913
  suggestedSource: childSource,
834
- displayName: deriveDisplayName(childSource)
914
+ displayName: metadata?.name || deriveDisplayName(childSource),
915
+ description: metadata?.description
835
916
  });
836
917
  indices.push(skillIndex);
837
918
  }
@@ -849,16 +930,19 @@ async function tryFetchSkillManifest(source, displayName, ctx) {
849
930
  if (res.status === 404) return [];
850
931
  return [];
851
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
+ ];
852
943
  } catch {
853
944
  return [];
854
945
  }
855
- return [
856
- registerSkill(ctx, {
857
- relPath: ".",
858
- suggestedSource: source,
859
- displayName: displayName || deriveDisplayName(source)
860
- })
861
- ];
862
946
  }
863
947
  function registerSkill(ctx, skill) {
864
948
  const key = skill.suggestedSource;
@@ -1053,12 +1137,18 @@ function getPlatformPromptList(scope) {
1053
1137
  const installed = getInstalledPlatforms(scope);
1054
1138
  return installed.length > 0 ? installed : [...PLATFORMS2];
1055
1139
  }
1056
- function asDiscoveredSkills(discovered, toSuggestedSource, toMaterializedDir) {
1057
- return discovered.map((d) => ({
1058
- relPath: d.relPath,
1059
- suggestedSource: toSuggestedSource(d),
1060
- materializedDir: toMaterializedDir ? toMaterializedDir(d) : void 0
1061
- }));
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
+ });
1062
1152
  }
1063
1153
  function createContext(source, options) {
1064
1154
  const scope = options.local ? "project" : "global";
@@ -1089,7 +1179,8 @@ function createContext(source, options) {
1089
1179
  jsonOnly,
1090
1180
  interactive,
1091
1181
  yes,
1092
- maxDepth: parseNonNegativeInt(options.depth, 0),
1182
+ markdownDepth: parseNonNegativeInt(options.depth, 0),
1183
+ scanDepth: parseNonNegativeInt(options.scanDepth, 6),
1093
1184
  maxSkills: parsePositiveInt(options.maxSkills, 200),
1094
1185
  resolvedSource: source.trim(),
1095
1186
  targets,
@@ -1136,7 +1227,7 @@ async function resolveSource(ctx) {
1136
1227
  return true;
1137
1228
  }
1138
1229
  async function discoverSkills(ctx) {
1139
- const { resolvedSource, maxDepth, maxSkills, jsonOnly } = ctx;
1230
+ const { resolvedSource, markdownDepth, scanDepth, maxSkills, jsonOnly } = ctx;
1140
1231
  if (ctx.spinner) {
1141
1232
  ctx.spinner.text = `Discovery at ${chalk3.cyan(ctx.source)}...`;
1142
1233
  }
@@ -1151,11 +1242,17 @@ async function discoverSkills(ctx) {
1151
1242
  if (isLocal) {
1152
1243
  const hasSkillMd2 = fs3.existsSync(path3.join(maybeLocalRoot, "SKILL.md"));
1153
1244
  if (hasSkillMd2) {
1245
+ const metadata = readSkillMetadata(maybeLocalRoot);
1154
1246
  ctx.isSingleSkill = true;
1155
- ctx.selectedSkills = [{ relPath: maybeLocalRoot, suggestedSource: resolvedSource }];
1247
+ ctx.selectedSkills = [{
1248
+ relPath: maybeLocalRoot,
1249
+ suggestedSource: resolvedSource,
1250
+ displayName: metadata?.name,
1251
+ description: metadata?.description
1252
+ }];
1156
1253
  return true;
1157
1254
  }
1158
- const discovered2 = discoverSkillDirsWithHeuristics(maybeLocalRoot, { maxDepth, maxSkills });
1255
+ const discovered2 = discoverSkillDirsWithHeuristics(maybeLocalRoot, { maxDepth: scanDepth, maxSkills });
1159
1256
  if (discovered2.length === 0) {
1160
1257
  const message = `No SKILL.md found at ${maybeLocalRoot} (or within subdirectories).`;
1161
1258
  if (jsonOnly) {
@@ -1167,12 +1264,18 @@ async function discoverSkills(ctx) {
1167
1264
  process.exitCode = 1;
1168
1265
  return false;
1169
1266
  }
1170
- 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
+ );
1171
1273
  return true;
1172
1274
  }
1173
1275
  const markdownResult = await discoverMarkdownSkillsFromSource({
1174
1276
  source: resolvedSource,
1175
- maxDepth,
1277
+ maxDocDepth: markdownDepth,
1278
+ maxSkillDepth: scanDepth,
1176
1279
  maxSkills,
1177
1280
  onProgress: (update2) => {
1178
1281
  if (ctx.spinner) {
@@ -1195,11 +1298,18 @@ async function discoverSkills(ctx) {
1195
1298
  ctx.cleanupMaterialized = appendCleanup(ctx.cleanupMaterialized, materialized.cleanup);
1196
1299
  const hasSkillMd = fs3.existsSync(path3.join(ctx.materializedDir, "SKILL.md"));
1197
1300
  if (hasSkillMd) {
1301
+ const metadata = readSkillMetadata(ctx.materializedDir);
1198
1302
  ctx.isSingleSkill = true;
1199
- 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
+ }];
1200
1310
  return true;
1201
1311
  }
1202
- const discovered = discoverSkillDirsWithHeuristics(ctx.materializedDir, { maxDepth, maxSkills });
1312
+ const discovered = discoverSkillDirsWithHeuristics(ctx.materializedDir, { maxDepth: scanDepth, maxSkills });
1203
1313
  if (discovered.length === 0) {
1204
1314
  const message = `No SKILL.md found in source "${resolvedSource}".`;
1205
1315
  if (jsonOnly) {
@@ -1214,6 +1324,7 @@ async function discoverSkills(ctx) {
1214
1324
  ctx.discoveredSkills = asDiscoveredSkills(
1215
1325
  discovered,
1216
1326
  (d) => deriveChildSource2(resolvedSource, d.relPath),
1327
+ (d) => d.absDir,
1217
1328
  (d) => d.absDir
1218
1329
  );
1219
1330
  return true;
@@ -2257,13 +2368,310 @@ async function search(query, options = {}) {
2257
2368
  }
2258
2369
  }
2259
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
+
2260
2668
  // src/index.ts
2261
2669
  import { PLATFORMS as PLATFORMS4 } from "@skild/core";
2262
2670
  var require2 = createRequire(import.meta.url);
2263
2671
  var { version } = require2("../package.json");
2264
2672
  var program = new Command();
2265
2673
  program.name("skild").description("The npm for Agent Skills \u2014 Discover, install, manage, and publish AI Agent Skills with ease.").version(version);
2266
- 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: 0)", "0").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) => {
2267
2675
  await install(source, options);
2268
2676
  });
2269
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));
@@ -2278,8 +2686,11 @@ program.command("logout").description("Remove stored registry credentials").acti
2278
2686
  program.command("whoami").description("Show current registry identity").action(async () => whoami());
2279
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));
2280
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
+ });
2281
2692
  program.action(() => {
2282
- 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"));
2283
2694
  program.outputHelp();
2284
2695
  });
2285
2696
  var argv = process.argv.slice();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skild",
3
- "version": "0.6.1",
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",