skild 0.5.2 → 0.6.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 (2) hide show
  1. package/dist/index.js +642 -41
  2. package/package.json +5 -1
package/dist/index.js CHANGED
@@ -6,17 +6,18 @@ import chalk16 from "chalk";
6
6
  import { createRequire } from "module";
7
7
 
8
8
  // src/commands/install.ts
9
- import fs2 from "fs";
10
- import path2 from "path";
9
+ import fs3 from "fs";
10
+ import path3 from "path";
11
11
  import chalk3 from "chalk";
12
12
  import {
13
- deriveChildSource,
14
- fetchWithTimeout,
13
+ deriveChildSource as deriveChildSource2,
14
+ fetchWithTimeout as fetchWithTimeout2,
15
15
  installRegistrySkill,
16
16
  installSkill,
17
17
  isValidAlias,
18
+ listSkills,
18
19
  loadRegistryAuth,
19
- materializeSourceToTemp,
20
+ materializeSourceToTemp as materializeSourceToTemp2,
20
21
  resolveRegistryAlias,
21
22
  resolveRegistryUrl,
22
23
  stripSourceRef,
@@ -70,10 +71,10 @@ var logger = {
70
71
  /**
71
72
  * Log a skill entry with status indicator.
72
73
  */
73
- skillEntry: (name, path4, hasSkillMd) => {
74
+ skillEntry: (name, path5, hasSkillMd) => {
74
75
  const status = hasSkillMd ? chalk.green("\u2713") : chalk.yellow("\u26A0");
75
76
  console.log(` ${status} ${chalk.cyan(name)}`);
76
- console.log(chalk.dim(` \u2514\u2500 ${path4}`));
77
+ console.log(chalk.dim(` \u2514\u2500 ${path5}`));
77
78
  },
78
79
  /**
79
80
  * Log installation result details.
@@ -119,6 +120,35 @@ function buildPlatformTree(items) {
119
120
  }
120
121
  return wrapWithRoot(allNode);
121
122
  }
123
+ function buildTreeFromSkillNodes(nodes, totalSkills) {
124
+ const allNode = createTreeNode("all", "All Skills", 1, false);
125
+ const attach = (node, depth) => {
126
+ const treeNode = createTreeNode(
127
+ node.id,
128
+ node.label,
129
+ depth,
130
+ Boolean(node.skillIndex != null && (!node.children || node.children.length === 0)),
131
+ node.skillIndex != null ? [node.skillIndex] : []
132
+ );
133
+ if (node.children?.length) {
134
+ for (const child of node.children) {
135
+ const childNode = attach(child, depth + 1);
136
+ treeNode.children.push(childNode);
137
+ treeNode.leafIndices.push(...childNode.leafIndices);
138
+ }
139
+ }
140
+ return treeNode;
141
+ };
142
+ for (const node of nodes) {
143
+ const childNode = attach(node, 2);
144
+ allNode.children.push(childNode);
145
+ allNode.leafIndices.push(...childNode.leafIndices);
146
+ }
147
+ if (allNode.leafIndices.length === 0 && totalSkills > 0) {
148
+ for (let i = 0; i < totalSkills; i++) allNode.leafIndices.push(i);
149
+ }
150
+ return wrapWithRoot(allNode);
151
+ }
122
152
  function createTreeNode(id, name, depth, isLeaf, leafIndices = []) {
123
153
  return { id, name, depth, children: [], leafIndices, isLeaf };
124
154
  }
@@ -353,8 +383,25 @@ async function promptSkillsInteractive(skills, options = {}) {
353
383
  `));
354
384
  return selectedSkills;
355
385
  }
386
+ async function promptSkillsTreeInteractive(skills, tree, options = {}) {
387
+ const selectedIndices = await interactiveTreeSelect(skills, {
388
+ title: "Select skills from markdown",
389
+ subtitle: "\u2191\u2193 navigate \u2022 Space toggle \u2022 Enter confirm",
390
+ buildTree: () => buildTreeFromSkillNodes(tree, skills.length),
391
+ formatNode: (node, selection, isCursor) => formatTreeNode(node, selection, isCursor),
392
+ defaultAll: options.defaultAll !== false
393
+ });
394
+ if (!selectedIndices) return null;
395
+ const selectedSkills = selectedIndices.map((i) => skills[i]);
396
+ const names = selectedSkills.map((s) => s.displayName || s.relPath || s.suggestedSource);
397
+ console.log(chalk2.green(`
398
+ \u2713 Selected ${selectedSkills.length} skill${selectedSkills.length > 1 ? "s" : ""}: ${chalk2.cyan(names.join(", "))}
399
+ `));
400
+ return selectedSkills;
401
+ }
356
402
  async function promptPlatformsInteractive(options = {}) {
357
- const platformItems = PLATFORMS.map((p) => ({ platform: p }));
403
+ const platforms = options.platforms && options.platforms.length > 0 ? options.platforms : PLATFORMS;
404
+ const platformItems = platforms.map((p) => ({ platform: p }));
358
405
  const selectedIndices = await interactiveTreeSelect(platformItems, {
359
406
  title: "Select target platforms",
360
407
  subtitle: "\u2191\u2193 navigate \u2022 Space toggle \u2022 Enter confirm",
@@ -367,7 +414,7 @@ async function promptPlatformsInteractive(options = {}) {
367
414
  defaultAll: options.defaultAll !== false
368
415
  });
369
416
  if (!selectedIndices) return null;
370
- const selected = selectedIndices.map((i) => PLATFORMS[i]);
417
+ const selected = selectedIndices.map((i) => platforms[i]);
371
418
  const names = selected.map((p) => PLATFORM_DISPLAY[p] || p);
372
419
  console.log(chalk2.green(`
373
420
  \u2713 Installing to ${selected.length} platform${selected.length > 1 ? "s" : ""}: ${chalk2.cyan(names.join(", "))}
@@ -457,6 +504,518 @@ function discoverSkillDirsWithHeuristics(rootDir, options) {
457
504
  return discoverSkillDirs(root, options);
458
505
  }
459
506
 
507
+ // src/commands/install-markdown.ts
508
+ import fs2 from "fs";
509
+ import path2 from "path";
510
+ import { unified } from "unified";
511
+ import remarkParse from "remark-parse";
512
+ import { toString } from "mdast-util-to-string";
513
+ import { deriveChildSource, fetchWithTimeout, materializeSourceToTemp } from "@skild/core";
514
+ var README_CANDIDATES = ["README.md", "readme.md", "Readme.md", "README.markdown", "readme.markdown"];
515
+ async function discoverMarkdownSkillsFromSource(input) {
516
+ const ctx = {
517
+ maxDepth: input.maxDepth,
518
+ maxSkills: input.maxSkills,
519
+ maxLinks: Math.min(400, Math.max(200, input.maxSkills * 2)),
520
+ linkLimitReached: false,
521
+ onProgress: input.onProgress,
522
+ docsScanned: 0,
523
+ linksChecked: 0,
524
+ skillsFound: 0,
525
+ repoCache: /* @__PURE__ */ new Map(),
526
+ skillCache: /* @__PURE__ */ new Map(),
527
+ skillIndexBySource: /* @__PURE__ */ new Map(),
528
+ skills: [],
529
+ skillCleanups: [],
530
+ nodeId: 0
531
+ };
532
+ try {
533
+ const entryDoc = await resolveMarkdownDoc(input.source, ctx);
534
+ if (!entryDoc) {
535
+ cleanupRepoCache(ctx);
536
+ return null;
537
+ }
538
+ const visitedDocs = /* @__PURE__ */ new Set();
539
+ const tree = await parseMarkdownDoc(entryDoc, ctx, 0, visitedDocs);
540
+ cleanupRepoCache(ctx);
541
+ const compacted = collapseSingleChildNodes(tree);
542
+ return {
543
+ skills: ctx.skills,
544
+ tree: compacted,
545
+ cleanup: () => {
546
+ for (const cleanup of ctx.skillCleanups) cleanup();
547
+ }
548
+ };
549
+ } catch {
550
+ cleanupRepoCache(ctx);
551
+ return null;
552
+ }
553
+ }
554
+ function cleanupRepoCache(ctx) {
555
+ for (const entry of ctx.repoCache.values()) {
556
+ entry.cleanup?.();
557
+ }
558
+ ctx.repoCache.clear();
559
+ }
560
+ async function parseMarkdownDoc(doc, ctx, depth, visitedDocs) {
561
+ if (depth > ctx.maxDepth) return [];
562
+ const docKey = `${doc.repo.owner}/${doc.repo.repo}#${doc.repo.ref || ""}:${doc.docPath}`;
563
+ if (visitedDocs.has(docKey)) return [];
564
+ visitedDocs.add(docKey);
565
+ let content;
566
+ try {
567
+ if (doc.content != null) {
568
+ content = doc.content;
569
+ } else if (doc.filePath) {
570
+ content = fs2.readFileSync(doc.filePath, "utf-8");
571
+ } else {
572
+ return [];
573
+ }
574
+ } catch {
575
+ return [];
576
+ }
577
+ ctx.docsScanned += 1;
578
+ updateProgress(ctx, doc.docPath ? path2.posix.basename(doc.docPath) : void 0);
579
+ const ast = unified().use(remarkParse).parse(content);
580
+ const rootNode = createNode(ctx, "root", "doc", []);
581
+ const headingStack = [];
582
+ for (const child of ast.children) {
583
+ if (child.type === "heading") {
584
+ const label = normalizeLabel(toString(child)) || `Section ${child.depth}`;
585
+ const headingNode = createNode(ctx, label, "heading", []);
586
+ const parent = findHeadingParent(headingStack, child.depth, rootNode);
587
+ parent.children.push(headingNode);
588
+ headingStack.push({ depth: child.depth, node: headingNode });
589
+ } else if (child.type === "list") {
590
+ const parent = currentHeading(headingStack, rootNode);
591
+ await parseListNode(child, parent, doc, ctx, depth, visitedDocs);
592
+ } else {
593
+ const parent = currentHeading(headingStack, rootNode);
594
+ await parseInlineLinks(child, parent, doc, ctx, depth, visitedDocs);
595
+ }
596
+ }
597
+ return rootNode.children;
598
+ }
599
+ function currentHeading(stack, fallback) {
600
+ return stack.length ? stack[stack.length - 1].node : fallback;
601
+ }
602
+ function findHeadingParent(stack, depth, fallback) {
603
+ while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
604
+ stack.pop();
605
+ }
606
+ return stack.length ? stack[stack.length - 1].node : fallback;
607
+ }
608
+ async function parseListNode(node, parent, doc, ctx, depth, visitedDocs) {
609
+ if (ctx.linkLimitReached || ctx.skillsFound >= ctx.maxSkills) return;
610
+ for (const item of node.children) {
611
+ const label = normalizeLabel(toString(item)) || "Item";
612
+ const listNode = createNode(ctx, label, "list", []);
613
+ let hasContent = false;
614
+ for (const child of item.children) {
615
+ if (child.type === "list") {
616
+ await parseListNode(child, listNode, doc, ctx, depth, visitedDocs);
617
+ if (listNode.children.length > 0) hasContent = true;
618
+ } else {
619
+ const added = await parseInlineLinks(child, listNode, doc, ctx, depth, visitedDocs);
620
+ if (added) hasContent = true;
621
+ }
622
+ }
623
+ if (hasContent) {
624
+ parent.children.push(listNode);
625
+ }
626
+ }
627
+ }
628
+ async function parseInlineLinks(node, parent, doc, ctx, depth, visitedDocs) {
629
+ const links = collectLinks(node);
630
+ let added = false;
631
+ if (ctx.skillsFound >= ctx.maxSkills) {
632
+ ctx.linkLimitReached = true;
633
+ updateProgress(ctx);
634
+ return false;
635
+ }
636
+ for (const link of links) {
637
+ if (ctx.linkLimitReached) return added;
638
+ if (ctx.linksChecked >= ctx.maxLinks) {
639
+ ctx.linkLimitReached = true;
640
+ updateProgress(ctx);
641
+ return added;
642
+ }
643
+ ctx.linksChecked += 1;
644
+ updateProgress(ctx);
645
+ const resolved = resolveLink(doc, link.url);
646
+ if (!resolved) continue;
647
+ const label = normalizeLabel(link.label) || resolved.displayName;
648
+ const maybeMarkdown = !isLikelyFilePath(resolved.pathHint) || looksLikeMarkdownPath(resolved.pathHint);
649
+ if (maybeMarkdown) {
650
+ const childDoc = await resolveMarkdownDoc(resolved.source, ctx);
651
+ if (childDoc) {
652
+ const childNodes = await parseMarkdownDoc(childDoc, ctx, depth + 1, visitedDocs);
653
+ if (childNodes.length > 0) {
654
+ const docNode = createNode(ctx, label, "doc", childNodes);
655
+ parent.children.push(docNode);
656
+ added = true;
657
+ continue;
658
+ }
659
+ }
660
+ }
661
+ if (isLikelyFilePath(resolved.pathHint)) continue;
662
+ const skillIndices = await resolveSkillsFromSource(resolved.source, label, ctx, resolved.sameRepo ? { repo: doc.repo, pathHint: resolved.pathHint } : void 0);
663
+ if (skillIndices.length === 0) continue;
664
+ added = true;
665
+ if (skillIndices.length === 1) {
666
+ parent.children.push(createSkillLeaf(ctx, skillIndices[0], label));
667
+ } else {
668
+ 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
+ }
673
+ parent.children.push(groupNode);
674
+ }
675
+ }
676
+ return added;
677
+ }
678
+ function createNode(ctx, label, kind, children) {
679
+ ctx.nodeId += 1;
680
+ return {
681
+ id: `md-${ctx.nodeId}`,
682
+ label: label.trim(),
683
+ kind,
684
+ children
685
+ };
686
+ }
687
+ function createSkillLeaf(ctx, skillIndex, labelOverride) {
688
+ const label = labelOverride?.trim() || ctx.skills[skillIndex]?.displayName || ctx.skills[skillIndex]?.relPath || "Skill";
689
+ const node = createNode(ctx, label, "skill", []);
690
+ node.skillIndex = skillIndex;
691
+ return node;
692
+ }
693
+ function collapseSingleChildNodes(nodes) {
694
+ const collapsed = [];
695
+ for (const node of nodes) {
696
+ const next = collapseNode(node);
697
+ if (next) collapsed.push(next);
698
+ }
699
+ return collapsed;
700
+ }
701
+ function collapseNode(node) {
702
+ node.children = node.children.map(collapseNode).filter(Boolean);
703
+ if (node.kind !== "heading" && node.kind !== "skill" && node.children.length === 1 && !node.skillIndex) {
704
+ return node.children[0];
705
+ }
706
+ if (node.kind !== "skill" && node.children.length === 0 && !node.skillIndex) {
707
+ return null;
708
+ }
709
+ return node;
710
+ }
711
+ function normalizeLabel(value) {
712
+ if (!value) return "";
713
+ return value.replace(/\s+/g, " ").trim();
714
+ }
715
+ function collectLinks(node) {
716
+ const links = [];
717
+ const visit = (n) => {
718
+ if (n.type === "link") {
719
+ const url = typeof n.url === "string" ? n.url : "";
720
+ if (url) {
721
+ links.push({ url, label: toString(n) });
722
+ }
723
+ return;
724
+ }
725
+ if ("children" in n && Array.isArray(n.children)) {
726
+ for (const child of n.children) {
727
+ visit(child);
728
+ }
729
+ }
730
+ };
731
+ visit(node);
732
+ return links;
733
+ }
734
+ function isLikelyFilePath(value) {
735
+ if (!value) return false;
736
+ const trimmed = value.split("?")[0].split("#")[0];
737
+ const base = trimmed.split("/").pop() || "";
738
+ if (!base.includes(".")) return false;
739
+ const lower = base.toLowerCase();
740
+ return !lower.endsWith(".md") && !lower.endsWith(".markdown");
741
+ }
742
+ function updateProgress(ctx, current) {
743
+ if (!ctx.onProgress) return;
744
+ ctx.onProgress({
745
+ docsScanned: ctx.docsScanned,
746
+ linksChecked: ctx.linksChecked,
747
+ skillsFound: ctx.skillsFound,
748
+ current,
749
+ linkLimitReached: ctx.linkLimitReached
750
+ });
751
+ }
752
+ async function resolveSkillsFromSource(source, displayName, ctx, repoHint) {
753
+ const cached = ctx.skillCache.get(source);
754
+ if (cached) return cached;
755
+ const localIndices = repoHint ? resolveSkillsFromLocal(repoHint, source, displayName, ctx) : [];
756
+ if (localIndices.length > 0) {
757
+ ctx.skillCache.set(source, localIndices);
758
+ return localIndices;
759
+ }
760
+ const quickIndex = await tryFetchSkillManifest(source, displayName, ctx);
761
+ if (quickIndex.length > 0) {
762
+ ctx.skillCache.set(source, quickIndex);
763
+ return quickIndex;
764
+ }
765
+ let materializedDir;
766
+ try {
767
+ const materialized = await materializeSourceToTemp(source);
768
+ ctx.skillCleanups.push(materialized.cleanup);
769
+ materializedDir = materialized.dir;
770
+ } catch {
771
+ ctx.skillCache.set(source, []);
772
+ return [];
773
+ }
774
+ const skillMd = path2.join(materializedDir, "SKILL.md");
775
+ if (fs2.existsSync(skillMd)) {
776
+ const skillIndex = registerSkill(ctx, {
777
+ relPath: ".",
778
+ suggestedSource: source,
779
+ materializedDir,
780
+ displayName: displayName || deriveDisplayName(source)
781
+ });
782
+ const indices2 = [skillIndex];
783
+ ctx.skillCache.set(source, indices2);
784
+ return indices2;
785
+ }
786
+ const discovered = discoverSkillDirsWithHeuristics(materializedDir, { maxDepth: ctx.maxDepth, maxSkills: ctx.maxSkills });
787
+ if (discovered.length === 0) {
788
+ ctx.skillCache.set(source, []);
789
+ return [];
790
+ }
791
+ const indices = [];
792
+ for (const skill of discovered) {
793
+ const childSource = deriveChildSource(source, skill.relPath);
794
+ const skillIndex = registerSkill(ctx, {
795
+ relPath: skill.relPath,
796
+ suggestedSource: childSource,
797
+ materializedDir: skill.absDir,
798
+ displayName: deriveDisplayName(childSource)
799
+ });
800
+ indices.push(skillIndex);
801
+ }
802
+ ctx.skillCache.set(source, indices);
803
+ return indices;
804
+ }
805
+ function resolveSkillsFromLocal(repoHint, source, displayName, ctx) {
806
+ if (!repoHint.repo.dir) return [];
807
+ const base = repoHint.pathHint ? path2.join(repoHint.repo.dir, repoHint.pathHint) : repoHint.repo.dir;
808
+ if (!fs2.existsSync(base)) return [];
809
+ const skillMd = path2.join(base, "SKILL.md");
810
+ if (fs2.existsSync(skillMd)) {
811
+ return [
812
+ registerSkill(ctx, {
813
+ relPath: ".",
814
+ suggestedSource: source,
815
+ displayName: displayName || deriveDisplayName(source)
816
+ })
817
+ ];
818
+ }
819
+ const discovered = discoverSkillDirsWithHeuristics(base, { maxDepth: ctx.maxDepth, maxSkills: ctx.maxSkills });
820
+ if (discovered.length === 0) return [];
821
+ const indices = [];
822
+ for (const skill of discovered) {
823
+ const childSource = deriveChildSource(source, skill.relPath);
824
+ const skillIndex = registerSkill(ctx, {
825
+ relPath: skill.relPath,
826
+ suggestedSource: childSource,
827
+ displayName: deriveDisplayName(childSource)
828
+ });
829
+ indices.push(skillIndex);
830
+ }
831
+ return indices;
832
+ }
833
+ async function tryFetchSkillManifest(source, displayName, ctx) {
834
+ const parsed = parseGitHubSource(source);
835
+ if (!parsed) return [];
836
+ const ref = parsed.ref || "HEAD";
837
+ const pathPrefix = parsed.path ? `${parsed.path.replace(/\/+$/, "")}/` : "";
838
+ const rawUrl = `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/${ref}/${pathPrefix}SKILL.md`;
839
+ try {
840
+ const res = await fetchWithTimeout(rawUrl, { method: "GET" }, 5e3);
841
+ if (!res.ok) {
842
+ if (res.status === 404) return [];
843
+ return [];
844
+ }
845
+ } catch {
846
+ return [];
847
+ }
848
+ return [
849
+ registerSkill(ctx, {
850
+ relPath: ".",
851
+ suggestedSource: source,
852
+ displayName: displayName || deriveDisplayName(source)
853
+ })
854
+ ];
855
+ }
856
+ function registerSkill(ctx, skill) {
857
+ const key = skill.suggestedSource;
858
+ const existing = ctx.skillIndexBySource.get(key);
859
+ if (existing != null) {
860
+ if (!ctx.skills[existing].displayName && skill.displayName) {
861
+ ctx.skills[existing].displayName = skill.displayName;
862
+ }
863
+ return existing;
864
+ }
865
+ if (ctx.skills.length >= ctx.maxSkills) {
866
+ ctx.linkLimitReached = true;
867
+ updateProgress(ctx);
868
+ return ctx.skills.length - 1;
869
+ }
870
+ ctx.skills.push(skill);
871
+ const index = ctx.skills.length - 1;
872
+ ctx.skillIndexBySource.set(key, index);
873
+ ctx.skillsFound = ctx.skills.length;
874
+ if (ctx.skillsFound >= ctx.maxSkills) {
875
+ ctx.linkLimitReached = true;
876
+ updateProgress(ctx);
877
+ }
878
+ return index;
879
+ }
880
+ function deriveDisplayName(source) {
881
+ const clean = source.replace(/[#?].*$/, "");
882
+ return clean.split("/").filter(Boolean).pop() || source;
883
+ }
884
+ function resolveLink(doc, url) {
885
+ const trimmed = url.trim();
886
+ if (!trimmed || trimmed.startsWith("#")) return null;
887
+ if (trimmed.startsWith("mailto:")) return null;
888
+ const parsed = parseGitHubSource(trimmed);
889
+ if (parsed) {
890
+ const source = toRepoSource(parsed);
891
+ const pathHint = parsed.path;
892
+ const displayName2 = parsed.path ? parsed.path.split("/").pop() || source : source;
893
+ const parsedRef = parsed.ref || doc.repo.ref || "HEAD";
894
+ const docRef = doc.repo.ref || "HEAD";
895
+ const sameRepo = parsed.owner === doc.repo.owner && parsed.repo === doc.repo.repo && parsedRef === docRef;
896
+ return { source, displayName: displayName2, pathHint, sameRepo };
897
+ }
898
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return null;
899
+ const relative = normalizeRepoRelative(trimmed);
900
+ if (!relative) return null;
901
+ const baseDir = path2.posix.dirname(doc.docPath);
902
+ const relativePath = relative.isAbsolute ? path2.posix.normalize(relative.path) : path2.posix.normalize(path2.posix.join(baseDir, relative.path));
903
+ const resolved = buildRepoSource({
904
+ owner: doc.repo.owner,
905
+ repo: doc.repo.repo,
906
+ ref: doc.repo.ref,
907
+ path: relativePath.replace(/^\/+/, "")
908
+ });
909
+ const displayName = relativePath.split("/").filter(Boolean).pop() || relative.path;
910
+ return { source: resolved, displayName, pathHint: relativePath, sameRepo: true };
911
+ }
912
+ function normalizeRepoRelative(link) {
913
+ const cleaned = link.split("#")[0].split("?")[0].trim();
914
+ if (!cleaned) return null;
915
+ if (cleaned.startsWith("/")) return { path: cleaned.slice(1), isAbsolute: true };
916
+ return { path: cleaned, isAbsolute: false };
917
+ }
918
+ function looksLikeMarkdownPath(value) {
919
+ if (!value) return false;
920
+ const lower = value.toLowerCase();
921
+ return lower.endsWith(".md") || lower.endsWith(".markdown") || README_CANDIDATES.some((name) => lower.endsWith(name.toLowerCase()));
922
+ }
923
+ function parseGitHubSource(input) {
924
+ if (input.includes("github.com") || input.includes("raw.githubusercontent.com")) {
925
+ try {
926
+ const url = new URL(input);
927
+ if (url.hostname === "raw.githubusercontent.com") {
928
+ const parts2 = url.pathname.split("/").filter(Boolean);
929
+ if (parts2.length < 4) return null;
930
+ const [owner2, repo2, ref2, ...rest2] = parts2;
931
+ return { owner: owner2, repo: repo2.replace(/\.git$/, ""), ref: ref2, path: rest2.join("/"), isFile: true };
932
+ }
933
+ if (url.hostname !== "github.com") return null;
934
+ const parts = url.pathname.split("/").filter(Boolean);
935
+ if (parts.length < 2) return null;
936
+ const [owner, repo, type, ref, ...rest] = parts;
937
+ if (!type) return { owner, repo: repo.replace(/\.git$/, "") };
938
+ if (type === "tree" || type === "blob") {
939
+ return { owner, repo: repo.replace(/\.git$/, ""), ref, path: rest.join("/"), isFile: type === "blob" };
940
+ }
941
+ return { owner, repo: repo.replace(/\.git$/, ""), path: [type, ref, ...rest].filter(Boolean).join("/") };
942
+ } catch {
943
+ return null;
944
+ }
945
+ }
946
+ if (/^[^/]+\/[^/]+/.test(input)) {
947
+ const [base, ref] = input.split("#", 2);
948
+ const parts = base.split("/").filter(Boolean);
949
+ if (parts.length < 2) return null;
950
+ const [owner, repo, ...rest] = parts;
951
+ const pathPart = rest.length ? rest.join("/") : void 0;
952
+ return { owner, repo, ref, path: pathPart, isFile: looksLikeMarkdownPath(pathPart) };
953
+ }
954
+ return null;
955
+ }
956
+ function toRepoSource(parsed) {
957
+ return buildRepoSource(parsed);
958
+ }
959
+ function buildRepoSource(parsed) {
960
+ const base = `${parsed.owner}/${parsed.repo}${parsed.path ? `/${parsed.path}` : ""}`;
961
+ return parsed.ref ? `${base}#${parsed.ref}` : base;
962
+ }
963
+ async function resolveMarkdownDoc(source, ctx) {
964
+ const parsed = parseGitHubSource(source);
965
+ if (!parsed) return null;
966
+ const ref = parsed.ref || "HEAD";
967
+ const repoKey = `${parsed.owner}/${parsed.repo}#${ref}`;
968
+ let repo = ctx.repoCache.get(repoKey);
969
+ if (!repo) {
970
+ repo = { owner: parsed.owner, repo: parsed.repo, ref };
971
+ ctx.repoCache.set(repoKey, repo);
972
+ }
973
+ const targetPath = parsed.path ? parsed.path.replace(/^\/+/, "") : "";
974
+ if (parsed.isFile || looksLikeMarkdownPath(targetPath)) {
975
+ const content = await fetchMarkdownContent(repo, targetPath);
976
+ if (!content) return null;
977
+ return { repo, docPath: targetPath, content };
978
+ }
979
+ const dirPath = targetPath;
980
+ for (const candidate of README_CANDIDATES) {
981
+ const docPath = dirPath ? path2.posix.join(dirPath, candidate) : candidate;
982
+ const content = await fetchMarkdownContent(repo, docPath);
983
+ if (content) return { repo, docPath, content };
984
+ }
985
+ const repoSpec = `${repo.owner}/${repo.repo}#${repo.ref}`;
986
+ try {
987
+ const materialized = await materializeSourceToTemp(repoSpec);
988
+ const localRepo = { ...repo, dir: materialized.dir, cleanup: materialized.cleanup };
989
+ ctx.repoCache.set(repoKey, localRepo);
990
+ const localTarget = targetPath;
991
+ if (parsed.isFile || looksLikeMarkdownPath(localTarget)) {
992
+ const filePath = path2.join(materialized.dir, localTarget);
993
+ if (fs2.existsSync(filePath)) return { repo: localRepo, docPath: localTarget, filePath };
994
+ return null;
995
+ }
996
+ for (const candidate of README_CANDIDATES) {
997
+ const filePath = path2.join(materialized.dir, dirPath, candidate);
998
+ if (fs2.existsSync(filePath)) {
999
+ return { repo: localRepo, docPath: path2.posix.join(dirPath, candidate), filePath };
1000
+ }
1001
+ }
1002
+ } catch {
1003
+ return null;
1004
+ }
1005
+ return null;
1006
+ }
1007
+ async function fetchMarkdownContent(repo, docPath) {
1008
+ const ref = repo.ref || "HEAD";
1009
+ const rawUrl = `https://raw.githubusercontent.com/${repo.owner}/${repo.repo}/${ref}/${docPath}`;
1010
+ try {
1011
+ const res = await fetchWithTimeout(rawUrl, { method: "GET" }, 5e3);
1012
+ if (!res.ok) return null;
1013
+ return await res.text();
1014
+ } catch {
1015
+ return null;
1016
+ }
1017
+ }
1018
+
460
1019
  // src/commands/install.ts
461
1020
  function looksLikeAlias(input) {
462
1021
  const s = input.trim();
@@ -464,7 +1023,7 @@ function looksLikeAlias(input) {
464
1023
  if (s.startsWith("@")) return false;
465
1024
  if (s.includes("/") || s.includes("\\")) return false;
466
1025
  if (/^https?:\/\//i.test(s) || s.includes("github.com")) return false;
467
- if (fs2.existsSync(path2.resolve(s))) return false;
1026
+ if (fs3.existsSync(path3.resolve(s))) return false;
468
1027
  if (!isValidAlias(s)) return false;
469
1028
  return true;
470
1029
  }
@@ -472,6 +1031,21 @@ function printJson(value) {
472
1031
  process.stdout.write(`${JSON.stringify(value, null, 2)}
473
1032
  `);
474
1033
  }
1034
+ function appendCleanup(current, next) {
1035
+ if (!next) return current;
1036
+ if (!current) return next;
1037
+ return () => {
1038
+ current();
1039
+ next();
1040
+ };
1041
+ }
1042
+ function getInstalledPlatforms(scope) {
1043
+ return PLATFORMS2.filter((platform) => listSkills({ platform, scope }).length > 0);
1044
+ }
1045
+ function getPlatformPromptList(scope) {
1046
+ const installed = getInstalledPlatforms(scope);
1047
+ return installed.length > 0 ? installed : [...PLATFORMS2];
1048
+ }
475
1049
  function asDiscoveredSkills(discovered, toSuggestedSource, toMaterializedDir) {
476
1050
  return discovered.map((d) => ({
477
1051
  relPath: d.relPath,
@@ -519,6 +1093,7 @@ function createContext(source, options) {
519
1093
  isSingleSkill: false,
520
1094
  materializedDir: null,
521
1095
  cleanupMaterialized: null,
1096
+ markdownTree: null,
522
1097
  results: [],
523
1098
  errors: [],
524
1099
  skipped: [],
@@ -564,10 +1139,10 @@ async function discoverSkills(ctx) {
564
1139
  ctx.selectedSkills = [{ relPath: resolvedSource, suggestedSource: resolvedSource }];
565
1140
  return true;
566
1141
  }
567
- const maybeLocalRoot = path2.resolve(resolvedSource);
568
- const isLocal = fs2.existsSync(maybeLocalRoot);
1142
+ const maybeLocalRoot = path3.resolve(resolvedSource);
1143
+ const isLocal = fs3.existsSync(maybeLocalRoot);
569
1144
  if (isLocal) {
570
- const hasSkillMd2 = fs2.existsSync(path2.join(maybeLocalRoot, "SKILL.md"));
1145
+ const hasSkillMd2 = fs3.existsSync(path3.join(maybeLocalRoot, "SKILL.md"));
571
1146
  if (hasSkillMd2) {
572
1147
  ctx.isSingleSkill = true;
573
1148
  ctx.selectedSkills = [{ relPath: maybeLocalRoot, suggestedSource: resolvedSource }];
@@ -585,13 +1160,33 @@ async function discoverSkills(ctx) {
585
1160
  process.exitCode = 1;
586
1161
  return false;
587
1162
  }
588
- ctx.discoveredSkills = asDiscoveredSkills(discovered2, (d) => path2.join(maybeLocalRoot, d.relPath));
1163
+ ctx.discoveredSkills = asDiscoveredSkills(discovered2, (d) => path3.join(maybeLocalRoot, d.relPath));
589
1164
  return true;
590
1165
  }
591
- const materialized = await materializeSourceToTemp(resolvedSource);
1166
+ const markdownResult = await discoverMarkdownSkillsFromSource({
1167
+ source: resolvedSource,
1168
+ maxDepth,
1169
+ maxSkills,
1170
+ onProgress: (update2) => {
1171
+ if (ctx.spinner) {
1172
+ const current = update2.current ? ` \xB7 ${update2.current}` : "";
1173
+ const capped = update2.linkLimitReached ? " \xB7 link cap reached" : "";
1174
+ ctx.spinner.text = `Parsing markdown (${update2.docsScanned} docs, ${update2.linksChecked} links, ${update2.skillsFound} skills)${current}${capped}`;
1175
+ }
1176
+ }
1177
+ });
1178
+ if (markdownResult && markdownResult.skills.length > 0) {
1179
+ ctx.discoveredSkills = markdownResult.skills;
1180
+ ctx.markdownTree = markdownResult.tree;
1181
+ ctx.isSingleSkill = markdownResult.skills.length === 1;
1182
+ ctx.selectedSkills = ctx.isSingleSkill ? [markdownResult.skills[0]] : null;
1183
+ ctx.cleanupMaterialized = appendCleanup(ctx.cleanupMaterialized, markdownResult.cleanup);
1184
+ return true;
1185
+ }
1186
+ const materialized = await materializeSourceToTemp2(resolvedSource);
592
1187
  ctx.materializedDir = materialized.dir;
593
- ctx.cleanupMaterialized = materialized.cleanup;
594
- const hasSkillMd = fs2.existsSync(path2.join(ctx.materializedDir, "SKILL.md"));
1188
+ ctx.cleanupMaterialized = appendCleanup(ctx.cleanupMaterialized, materialized.cleanup);
1189
+ const hasSkillMd = fs3.existsSync(path3.join(ctx.materializedDir, "SKILL.md"));
595
1190
  if (hasSkillMd) {
596
1191
  ctx.isSingleSkill = true;
597
1192
  ctx.selectedSkills = [{ relPath: ".", suggestedSource: resolvedSource, materializedDir: ctx.materializedDir }];
@@ -611,7 +1206,7 @@ async function discoverSkills(ctx) {
611
1206
  }
612
1207
  ctx.discoveredSkills = asDiscoveredSkills(
613
1208
  discovered,
614
- (d) => deriveChildSource(resolvedSource, d.relPath),
1209
+ (d) => deriveChildSource2(resolvedSource, d.relPath),
615
1210
  (d) => d.absDir
616
1211
  );
617
1212
  return true;
@@ -631,7 +1226,10 @@ async function promptSelections(ctx) {
631
1226
  if (isSingleSkill) {
632
1227
  if (ctx.needsPlatformPrompt) {
633
1228
  if (ctx.spinner) ctx.spinner.stop();
634
- const selectedPlatforms = await promptPlatformsInteractive({ defaultAll: true });
1229
+ const selectedPlatforms = await promptPlatformsInteractive({
1230
+ defaultAll: true,
1231
+ platforms: getPlatformPromptList(ctx.scope)
1232
+ });
635
1233
  if (!selectedPlatforms) {
636
1234
  console.log(chalk3.red("No platforms selected."));
637
1235
  process.exitCode = 1;
@@ -685,7 +1283,7 @@ async function promptSelections(ctx) {
685
1283
  `;
686
1284
  console.log(chalk3.yellow(headline));
687
1285
  }
688
- const selected = await promptSkillsInteractive(discoveredSkills, { defaultAll: true });
1286
+ const selected = ctx.markdownTree ? await promptSkillsTreeInteractive(discoveredSkills, ctx.markdownTree, { defaultAll: true }) : await promptSkillsInteractive(discoveredSkills, { defaultAll: true });
689
1287
  if (!selected) {
690
1288
  console.log(chalk3.red("No skills selected."));
691
1289
  process.exitCode = 1;
@@ -693,7 +1291,10 @@ async function promptSelections(ctx) {
693
1291
  }
694
1292
  ctx.selectedSkills = selected;
695
1293
  if (ctx.needsPlatformPrompt) {
696
- const selectedPlatforms = await promptPlatformsInteractive({ defaultAll: true });
1294
+ const selectedPlatforms = await promptPlatformsInteractive({
1295
+ defaultAll: true,
1296
+ platforms: getPlatformPromptList(ctx.scope)
1297
+ });
697
1298
  if (!selectedPlatforms) {
698
1299
  console.log(chalk3.red("No platforms selected."));
699
1300
  process.exitCode = 1;
@@ -905,7 +1506,7 @@ async function reportDownload(record, registryOverride) {
905
1506
  } else {
906
1507
  return;
907
1508
  }
908
- await fetchWithTimeout(
1509
+ await fetchWithTimeout2(
909
1510
  endpoint,
910
1511
  {
911
1512
  method: "POST",
@@ -920,7 +1521,7 @@ async function reportDownload(record, registryOverride) {
920
1521
 
921
1522
  // src/commands/list.ts
922
1523
  import chalk4 from "chalk";
923
- import { PLATFORMS as PLATFORMS3, listAllSkills, listSkills } from "@skild/core";
1524
+ import { PLATFORMS as PLATFORMS3, listAllSkills, listSkills as listSkills2 } from "@skild/core";
924
1525
  function isSkillset(skill) {
925
1526
  return Boolean(skill.record?.skillset || skill.record?.skill?.frontmatter?.skillset);
926
1527
  }
@@ -1019,7 +1620,7 @@ async function list(options = {}) {
1019
1620
  const verbose = Boolean(options.verbose);
1020
1621
  const platform = options.target;
1021
1622
  if (platform) {
1022
- const skills = listSkills({ platform, scope });
1623
+ const skills = listSkills2({ platform, scope });
1023
1624
  if (options.json) {
1024
1625
  console.log(JSON.stringify(skills, null, 2));
1025
1626
  return;
@@ -1185,7 +1786,7 @@ async function init(name, options = {}) {
1185
1786
 
1186
1787
  // src/commands/signup.ts
1187
1788
  import chalk10 from "chalk";
1188
- import { fetchWithTimeout as fetchWithTimeout2, resolveRegistryUrl as resolveRegistryUrl2, SkildError as SkildError6 } from "@skild/core";
1789
+ import { fetchWithTimeout as fetchWithTimeout3, resolveRegistryUrl as resolveRegistryUrl2, SkildError as SkildError6 } from "@skild/core";
1189
1790
 
1190
1791
  // src/utils/prompt.ts
1191
1792
  import readline2 from "readline";
@@ -1273,7 +1874,7 @@ async function signup(options) {
1273
1874
  }
1274
1875
  let text = "";
1275
1876
  try {
1276
- const res = await fetchWithTimeout2(
1877
+ const res = await fetchWithTimeout3(
1277
1878
  `${registry}/auth/signup`,
1278
1879
  {
1279
1880
  method: "POST",
@@ -1325,7 +1926,7 @@ async function signup(options) {
1325
1926
 
1326
1927
  // src/commands/login.ts
1327
1928
  import chalk11 from "chalk";
1328
- import { fetchWithTimeout as fetchWithTimeout3, resolveRegistryUrl as resolveRegistryUrl3, saveRegistryAuth, SkildError as SkildError7 } from "@skild/core";
1929
+ import { fetchWithTimeout as fetchWithTimeout4, resolveRegistryUrl as resolveRegistryUrl3, saveRegistryAuth, SkildError as SkildError7 } from "@skild/core";
1329
1930
  async function login(options) {
1330
1931
  const registry = resolveRegistryUrl3(options.registry);
1331
1932
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
@@ -1352,7 +1953,7 @@ async function login(options) {
1352
1953
  const finalTokenName = options.tokenName?.trim() || void 0;
1353
1954
  let text = "";
1354
1955
  try {
1355
- const res = await fetchWithTimeout3(
1956
+ const res = await fetchWithTimeout4(
1356
1957
  `${registry}/auth/login`,
1357
1958
  {
1358
1959
  method: "POST",
@@ -1406,7 +2007,7 @@ async function logout() {
1406
2007
 
1407
2008
  // src/commands/whoami.ts
1408
2009
  import chalk13 from "chalk";
1409
- import { fetchWithTimeout as fetchWithTimeout4, loadRegistryAuth as loadRegistryAuth2, resolveRegistryUrl as resolveRegistryUrl4, SkildError as SkildError8 } from "@skild/core";
2010
+ import { fetchWithTimeout as fetchWithTimeout5, loadRegistryAuth as loadRegistryAuth2, resolveRegistryUrl as resolveRegistryUrl4, SkildError as SkildError8 } from "@skild/core";
1410
2011
  async function whoami() {
1411
2012
  const auth = loadRegistryAuth2();
1412
2013
  if (!auth) {
@@ -1416,7 +2017,7 @@ async function whoami() {
1416
2017
  }
1417
2018
  const registryUrl = resolveRegistryUrl4(auth.registryUrl);
1418
2019
  try {
1419
- const res = await fetchWithTimeout4(
2020
+ const res = await fetchWithTimeout5(
1420
2021
  `${registryUrl}/auth/me`,
1421
2022
  { headers: { authorization: `Bearer ${auth.token}`, accept: "application/json" } },
1422
2023
  5e3
@@ -1438,13 +2039,13 @@ async function whoami() {
1438
2039
  }
1439
2040
 
1440
2041
  // src/commands/publish.ts
1441
- import fs3 from "fs";
2042
+ import fs4 from "fs";
1442
2043
  import os from "os";
1443
- import path3 from "path";
2044
+ import path4 from "path";
1444
2045
  import crypto from "crypto";
1445
2046
  import * as tar from "tar";
1446
2047
  import chalk14 from "chalk";
1447
- import { assertValidAlias, fetchWithTimeout as fetchWithTimeout5, loadRegistryAuth as loadRegistryAuth3, normalizeAlias, resolveRegistryUrl as resolveRegistryUrl5, SkildError as SkildError9, splitCanonicalName, validateSkillDir } from "@skild/core";
2048
+ import { assertValidAlias, fetchWithTimeout as fetchWithTimeout6, loadRegistryAuth as loadRegistryAuth3, normalizeAlias, resolveRegistryUrl as resolveRegistryUrl5, SkildError as SkildError9, splitCanonicalName, validateSkillDir } from "@skild/core";
1448
2049
  function sha256Hex(buf) {
1449
2050
  const h = crypto.createHash("sha256");
1450
2051
  h.update(buf);
@@ -1463,7 +2064,7 @@ async function publish(options = {}) {
1463
2064
  process.exitCode = 1;
1464
2065
  return;
1465
2066
  }
1466
- const dir = path3.resolve(options.dir || process.cwd());
2067
+ const dir = path4.resolve(options.dir || process.cwd());
1467
2068
  const validation = validateSkillDir(dir);
1468
2069
  if (!validation.ok) {
1469
2070
  console.error(chalk14.red("Skill validation failed:"));
@@ -1492,7 +2093,7 @@ async function publish(options = {}) {
1492
2093
  process.exitCode = 1;
1493
2094
  return;
1494
2095
  }
1495
- const meRes = await fetchWithTimeout5(
2096
+ const meRes = await fetchWithTimeout6(
1496
2097
  `${registry}/auth/me`,
1497
2098
  { headers: { authorization: `Bearer ${token}` } },
1498
2099
  1e4
@@ -1533,8 +2134,8 @@ async function publish(options = {}) {
1533
2134
  }
1534
2135
  }
1535
2136
  const spinner = createSpinner(`Publishing ${chalk14.cyan(`${name}@${version2}`)} to ${chalk14.dim(registry)}...`);
1536
- const tempDir = fs3.mkdtempSync(path3.join(os.tmpdir(), "skild-publish-"));
1537
- const tarballPath = path3.join(tempDir, "skill.tgz");
2137
+ const tempDir = fs4.mkdtempSync(path4.join(os.tmpdir(), "skild-publish-"));
2138
+ const tarballPath = path4.join(tempDir, "skill.tgz");
1538
2139
  try {
1539
2140
  await tar.c(
1540
2141
  {
@@ -1546,7 +2147,7 @@ async function publish(options = {}) {
1546
2147
  },
1547
2148
  ["."]
1548
2149
  );
1549
- const buf = fs3.readFileSync(tarballPath);
2150
+ const buf = fs4.readFileSync(tarballPath);
1550
2151
  const integrity = sha256Hex(buf);
1551
2152
  const form = new FormData();
1552
2153
  form.set("version", version2);
@@ -1557,7 +2158,7 @@ async function publish(options = {}) {
1557
2158
  form.set("dependencies", JSON.stringify(dependencies));
1558
2159
  form.append("tarball", new Blob([buf], { type: "application/gzip" }), "skill.tgz");
1559
2160
  const { scope, name: skillName } = splitCanonicalName(name);
1560
- const res = await fetchWithTimeout5(
2161
+ const res = await fetchWithTimeout6(
1561
2162
  `${registry}/skills/${encodeURIComponent(scope)}/${encodeURIComponent(skillName)}/publish`,
1562
2163
  {
1563
2164
  method: "POST",
@@ -1575,7 +2176,7 @@ async function publish(options = {}) {
1575
2176
  }
1576
2177
  if (alias) {
1577
2178
  spinner.text = `Publishing ${chalk14.cyan(`${name}@${version2}`)} \u2014 setting alias ${chalk14.cyan(alias)}...`;
1578
- const aliasRes = await fetchWithTimeout5(
2179
+ const aliasRes = await fetchWithTimeout6(
1579
2180
  `${registry}/publisher/skills/${encodeURIComponent(scope)}/${encodeURIComponent(skillName)}/alias`,
1580
2181
  {
1581
2182
  method: "POST",
@@ -1613,7 +2214,7 @@ async function publish(options = {}) {
1613
2214
  console.error(chalk14.red(message));
1614
2215
  process.exitCode = 1;
1615
2216
  } finally {
1616
- fs3.rmSync(tempDir, { recursive: true, force: true });
2217
+ fs4.rmSync(tempDir, { recursive: true, force: true });
1617
2218
  }
1618
2219
  }
1619
2220
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skild",
3
- "version": "0.5.2",
3
+ "version": "0.6.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",
@@ -36,11 +36,15 @@
36
36
  "@inquirer/prompts": "^8.2.0",
37
37
  "chalk": "^5.3.0",
38
38
  "commander": "^12.1.0",
39
+ "mdast-util-to-string": "^4.0.0",
39
40
  "ora": "^8.0.1",
41
+ "remark-parse": "^11.0.0",
40
42
  "tar": "^7.4.3",
43
+ "unified": "^11.0.4",
41
44
  "@skild/core": "^0.5.2"
42
45
  },
43
46
  "devDependencies": {
47
+ "@types/mdast": "^4.0.4",
44
48
  "@types/node": "^20.10.0",
45
49
  "tsup": "^8.0.0",
46
50
  "typescript": "^5.3.0"