playbooks 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +38 -2
  2. package/dist/index.js +510 -78
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -39,7 +39,7 @@ Skills let your agents perform specialized tasks like:
39
39
 
40
40
  ## Usage
41
41
 
42
- Playbooks uses an action/type command structure:
42
+ playbooks uses an action/type command structure:
43
43
  - `npx playbooks add skill <source>`
44
44
  - `npx playbooks find skill`
45
45
  - `npx playbooks list skill`
@@ -94,6 +94,12 @@ npx playbooks add skill git@github.com:anthropics/skills.git
94
94
  npx playbooks add skill https://docs.example.com/skills/my-skill/SKILL.md
95
95
  ```
96
96
 
97
+ - Docs URL (well-known skills discovery)
98
+ ```bash
99
+ npx playbooks add skill https://mintlify.com/docs
100
+ npx playbooks add skill mintlify.com/docs
101
+ ```
102
+
97
103
  - Marketplace.json (path)
98
104
  ```bash
99
105
  npx playbooks add skill ./path/to/.claude-plugin/marketplace.json
@@ -109,6 +115,36 @@ npx playbooks add skill https://raw.githubusercontent.com/org/repo/main/.claude-
109
115
  npx playbooks add skill org/repo/.claude-plugin/marketplace.json
110
116
  ```
111
117
 
118
+ ### Well-known skills discovery (RFC 8615)
119
+
120
+ If a docs site publishes a skills index at a predictable path, playbooks can discover and install skills from the site URL directly. The CLI looks for:
121
+
122
+ ```text
123
+ https://example.com/docs/.well-known/skills/index.json
124
+ ```
125
+
126
+ The index lists one or more skills and the files for each skill:
127
+
128
+ ```json
129
+ {
130
+ "skills": [
131
+ {
132
+ "name": "mintlify",
133
+ "description": "Build and maintain documentation sites with Mintlify.",
134
+ "files": ["SKILL.md"]
135
+ }
136
+ ]
137
+ }
138
+ ```
139
+
140
+ When you run `npx playbooks add skill <docs-url>`, playbooks fetches the index and then downloads each skill from:
141
+
142
+ ```text
143
+ https://example.com/docs/.well-known/skills/<skill-name>/SKILL.md
144
+ ```
145
+
146
+ Multiple skills can be listed in the same index and will be shown in the selection screen.
147
+
112
148
  ### Options (add skill)
113
149
 
114
150
  | Option | Description |
@@ -167,7 +203,7 @@ npx playbooks manage skill
167
203
 
168
204
  ## Marketplace.json support
169
205
 
170
- Playbooks can ingest a Claude-style `marketplace.json` and pull skills from the plugins it lists.
206
+ playbooks can ingest a Claude-style `marketplace.json` and pull skills from the plugins it lists.
171
207
 
172
208
  What it scans:
173
209
  - The plugin root (if it contains `SKILL.md`)
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { program } from "commander";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "playbooks",
11
- version: "0.1.7",
11
+ version: "0.1.9",
12
12
  description: "Install agent skills, MCPs and docs into your coding agents from any git repository.",
13
13
  type: "module",
14
14
  bin: {
@@ -1759,8 +1759,9 @@ function isDirectSkillUrl(input) {
1759
1759
  return true;
1760
1760
  }
1761
1761
  function parseSource(input) {
1762
- if (isLocalPath(input)) {
1763
- const resolvedPath = resolve4(input);
1762
+ let normalizedInput = input;
1763
+ if (isLocalPath(normalizedInput)) {
1764
+ const resolvedPath = resolve4(normalizedInput);
1764
1765
  return {
1765
1766
  type: "local",
1766
1767
  url: resolvedPath,
@@ -1768,13 +1769,18 @@ function parseSource(input) {
1768
1769
  localPath: resolvedPath
1769
1770
  };
1770
1771
  }
1771
- if (isDirectSkillUrl(input)) {
1772
+ if (shouldPrefixHttps(normalizedInput)) {
1773
+ normalizedInput = `https://${normalizedInput}`;
1774
+ }
1775
+ if (isDirectSkillUrl(normalizedInput)) {
1772
1776
  return {
1773
1777
  type: "direct-url",
1774
- url: input
1778
+ url: normalizedInput
1775
1779
  };
1776
1780
  }
1777
- const githubTreeWithPathMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
1781
+ const githubTreeWithPathMatch = normalizedInput.match(
1782
+ /github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/
1783
+ );
1778
1784
  if (githubTreeWithPathMatch) {
1779
1785
  const [, owner, repo, ref, subpath] = githubTreeWithPathMatch;
1780
1786
  return {
@@ -1784,7 +1790,7 @@ function parseSource(input) {
1784
1790
  subpath
1785
1791
  };
1786
1792
  }
1787
- const githubTreeMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/);
1793
+ const githubTreeMatch = normalizedInput.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/);
1788
1794
  if (githubTreeMatch) {
1789
1795
  const [, owner, repo, ref] = githubTreeMatch;
1790
1796
  return {
@@ -1793,7 +1799,7 @@ function parseSource(input) {
1793
1799
  ref
1794
1800
  };
1795
1801
  }
1796
- const githubRepoMatch = input.match(/github\.com\/([^/]+)\/([^/]+)/);
1802
+ const githubRepoMatch = normalizedInput.match(/github\.com\/([^/]+)\/([^/]+)/);
1797
1803
  if (githubRepoMatch) {
1798
1804
  const [, owner, repo] = githubRepoMatch;
1799
1805
  const cleanRepo = repo?.replace(/\.git$/, "");
@@ -1802,7 +1808,7 @@ function parseSource(input) {
1802
1808
  url: `https://github.com/${owner}/${cleanRepo}.git`
1803
1809
  };
1804
1810
  }
1805
- const gitlabTreeWithPathMatch = input.match(
1811
+ const gitlabTreeWithPathMatch = normalizedInput.match(
1806
1812
  /gitlab\.com\/([^/]+)\/([^/]+)\/-\/tree\/([^/]+)\/(.+)/
1807
1813
  );
1808
1814
  if (gitlabTreeWithPathMatch) {
@@ -1814,7 +1820,7 @@ function parseSource(input) {
1814
1820
  subpath
1815
1821
  };
1816
1822
  }
1817
- const gitlabTreeMatch = input.match(/gitlab\.com\/([^/]+)\/([^/]+)\/-\/tree\/([^/]+)$/);
1823
+ const gitlabTreeMatch = normalizedInput.match(/gitlab\.com\/([^/]+)\/([^/]+)\/-\/tree\/([^/]+)$/);
1818
1824
  if (gitlabTreeMatch) {
1819
1825
  const [, owner, repo, ref] = gitlabTreeMatch;
1820
1826
  return {
@@ -1823,7 +1829,7 @@ function parseSource(input) {
1823
1829
  ref
1824
1830
  };
1825
1831
  }
1826
- const gitlabRepoMatch = input.match(/gitlab\.com\/([^/]+)\/([^/]+)/);
1832
+ const gitlabRepoMatch = normalizedInput.match(/gitlab\.com\/([^/]+)\/([^/]+)/);
1827
1833
  if (gitlabRepoMatch) {
1828
1834
  const [, owner, repo] = gitlabRepoMatch;
1829
1835
  const cleanRepo = repo?.replace(/\.git$/, "");
@@ -1832,8 +1838,8 @@ function parseSource(input) {
1832
1838
  url: `https://gitlab.com/${owner}/${cleanRepo}.git`
1833
1839
  };
1834
1840
  }
1835
- const shorthandMatch = input.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/);
1836
- if (shorthandMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) {
1841
+ const shorthandMatch = normalizedInput.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/);
1842
+ if (shorthandMatch && !normalizedInput.includes(":") && !normalizedInput.startsWith(".") && !normalizedInput.startsWith("/")) {
1837
1843
  const [, owner, repo, subpath] = shorthandMatch;
1838
1844
  return {
1839
1845
  type: "github",
@@ -1841,11 +1847,53 @@ function parseSource(input) {
1841
1847
  subpath
1842
1848
  };
1843
1849
  }
1850
+ if (isWellKnownUrl(normalizedInput)) {
1851
+ return {
1852
+ type: "well-known",
1853
+ url: normalizedInput
1854
+ };
1855
+ }
1844
1856
  return {
1845
1857
  type: "git",
1846
- url: input
1858
+ url: normalizedInput
1847
1859
  };
1848
1860
  }
1861
+ function isWellKnownUrl(input) {
1862
+ if (!input.startsWith("http://") && !input.startsWith("https://")) {
1863
+ return false;
1864
+ }
1865
+ try {
1866
+ const parsed = new URL(input);
1867
+ const excludedHosts = [
1868
+ "github.com",
1869
+ "gitlab.com",
1870
+ "huggingface.co",
1871
+ "raw.githubusercontent.com"
1872
+ ];
1873
+ if (excludedHosts.includes(parsed.hostname)) {
1874
+ return false;
1875
+ }
1876
+ if (input.toLowerCase().endsWith("/skill.md")) {
1877
+ return false;
1878
+ }
1879
+ if (input.endsWith(".git")) {
1880
+ return false;
1881
+ }
1882
+ return true;
1883
+ } catch {
1884
+ return false;
1885
+ }
1886
+ }
1887
+ function shouldPrefixHttps(input) {
1888
+ if (input.startsWith("http://") || input.startsWith("https://")) {
1889
+ return false;
1890
+ }
1891
+ const firstSegment = input.split("/")[0] ?? "";
1892
+ if (!firstSegment || firstSegment.includes("@")) {
1893
+ return false;
1894
+ }
1895
+ return firstSegment.includes(".");
1896
+ }
1849
1897
 
1850
1898
  // src/flows/install-tracking.ts
1851
1899
  async function recordMarketplaceTracking(skills, results, installGlobally, originBySkillName) {
@@ -2363,9 +2411,9 @@ function AddScopeScreen() {
2363
2411
 
2364
2412
  // src/tui/screens/AddSkillSelect.tsx
2365
2413
  import { existsSync as existsSync5 } from "fs";
2366
- import { mkdir as mkdir4, mkdtemp as mkdtemp3, writeFile as writeFile3 } from "fs/promises";
2367
- import { tmpdir as tmpdir4 } from "os";
2368
- import { basename as basename3, join as join12 } from "path";
2414
+ import { mkdir as mkdir5, mkdtemp as mkdtemp4, writeFile as writeFile4 } from "fs/promises";
2415
+ import { tmpdir as tmpdir5 } from "os";
2416
+ import { join as join13 } from "path";
2369
2417
  import { Box as Box15, Text as Text13 } from "ink";
2370
2418
  import React11 from "react";
2371
2419
 
@@ -2620,9 +2668,231 @@ var RawSkillProvider = class {
2620
2668
  };
2621
2669
  var rawSkillProvider = new RawSkillProvider();
2622
2670
 
2671
+ // src/providers/wellknown.ts
2672
+ import matter6 from "gray-matter";
2673
+ var WellKnownProvider = class {
2674
+ id = "well-known";
2675
+ displayName = "Well-Known Skills";
2676
+ WELL_KNOWN_PATH = ".well-known/skills";
2677
+ INDEX_FILE = "index.json";
2678
+ match(url) {
2679
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
2680
+ return { matches: false };
2681
+ }
2682
+ if (!url.includes(`/${this.WELL_KNOWN_PATH}/`)) {
2683
+ return { matches: false };
2684
+ }
2685
+ return { matches: true, sourceIdentifier: this.getSourceIdentifier(url) };
2686
+ }
2687
+ async fetchIndex(baseUrl) {
2688
+ try {
2689
+ const parsed = new URL(baseUrl);
2690
+ const basePath = parsed.pathname.replace(/\/$/, "");
2691
+ const urlsToTry = [
2692
+ {
2693
+ indexUrl: `${parsed.protocol}//${parsed.host}${basePath}/${this.WELL_KNOWN_PATH}/${this.INDEX_FILE}`,
2694
+ baseUrl: `${parsed.protocol}//${parsed.host}${basePath}`
2695
+ }
2696
+ ];
2697
+ if (basePath && basePath !== "") {
2698
+ urlsToTry.push({
2699
+ indexUrl: `${parsed.protocol}//${parsed.host}/${this.WELL_KNOWN_PATH}/${this.INDEX_FILE}`,
2700
+ baseUrl: `${parsed.protocol}//${parsed.host}`
2701
+ });
2702
+ }
2703
+ for (const { indexUrl, baseUrl: resolvedBase } of urlsToTry) {
2704
+ try {
2705
+ const response = await fetch(indexUrl);
2706
+ if (!response.ok) {
2707
+ continue;
2708
+ }
2709
+ const index = await response.json();
2710
+ if (!index.skills || !Array.isArray(index.skills)) {
2711
+ continue;
2712
+ }
2713
+ let allValid = true;
2714
+ for (const entry of index.skills) {
2715
+ if (!this.isValidSkillEntry(entry)) {
2716
+ allValid = false;
2717
+ break;
2718
+ }
2719
+ }
2720
+ if (allValid) {
2721
+ return { index, resolvedBaseUrl: resolvedBase };
2722
+ }
2723
+ } catch {
2724
+ }
2725
+ }
2726
+ return null;
2727
+ } catch {
2728
+ return null;
2729
+ }
2730
+ }
2731
+ isValidSkillEntry(entry) {
2732
+ if (!entry || typeof entry !== "object") return false;
2733
+ const e = entry;
2734
+ if (typeof e.name !== "string" || !e.name) return false;
2735
+ if (typeof e.description !== "string" || !e.description) return false;
2736
+ if (!Array.isArray(e.files) || e.files.length === 0) return false;
2737
+ const nameRegex = /^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/;
2738
+ if (!nameRegex.test(e.name) && e.name.length > 1) {
2739
+ if (e.name.length === 1 && !/^[a-z0-9]$/.test(e.name)) {
2740
+ return false;
2741
+ }
2742
+ }
2743
+ for (const file of e.files) {
2744
+ if (typeof file !== "string") return false;
2745
+ if (file.startsWith("/") || file.startsWith("\\") || file.includes("..")) return false;
2746
+ }
2747
+ const hasSkillMd2 = e.files.some((f) => typeof f === "string" && f.toLowerCase() === "skill.md");
2748
+ if (!hasSkillMd2) return false;
2749
+ return true;
2750
+ }
2751
+ async fetchSkill(url) {
2752
+ try {
2753
+ const parsed = new URL(url);
2754
+ const skillMatch = parsed.pathname.match(/^(.*)\/\.well-known\/skills\/([^/]+)\/SKILL\.md$/i);
2755
+ if (skillMatch) {
2756
+ const baseUrl = `${parsed.protocol}//${parsed.host}${skillMatch[1] || ""}`;
2757
+ const skillName2 = skillMatch[2];
2758
+ const result2 = await this.fetchIndex(baseUrl);
2759
+ if (result2) {
2760
+ const entry = result2.index.skills.find((s) => s.name === skillName2);
2761
+ if (entry) {
2762
+ const fetched = await this.fetchSkillByEntry(result2.resolvedBaseUrl, entry);
2763
+ if (fetched) {
2764
+ return fetched;
2765
+ }
2766
+ }
2767
+ }
2768
+ }
2769
+ const result = await this.fetchIndex(url);
2770
+ if (!result) {
2771
+ return null;
2772
+ }
2773
+ const { index, resolvedBaseUrl } = result;
2774
+ let skillName = null;
2775
+ const pathMatch = parsed.pathname.match(/\/.well-known\/skills\/([^/]+)\/?$/);
2776
+ if (pathMatch?.[1] && pathMatch[1] !== "index.json") {
2777
+ skillName = pathMatch[1];
2778
+ } else if (index.skills.length === 1) {
2779
+ skillName = index.skills[0]?.name ?? null;
2780
+ }
2781
+ if (!skillName) {
2782
+ return null;
2783
+ }
2784
+ const skillEntry = index.skills.find((s) => s.name === skillName);
2785
+ if (!skillEntry) {
2786
+ return null;
2787
+ }
2788
+ return await this.fetchSkillByEntry(resolvedBaseUrl, skillEntry);
2789
+ } catch {
2790
+ return null;
2791
+ }
2792
+ }
2793
+ async fetchSkillByEntry(baseUrl, entry) {
2794
+ try {
2795
+ const skillBaseUrl = `${baseUrl.replace(/\/$/, "")}/${this.WELL_KNOWN_PATH}/${entry.name}`;
2796
+ const skillMdUrl = `${skillBaseUrl}/SKILL.md`;
2797
+ const response = await fetch(skillMdUrl);
2798
+ if (!response.ok) {
2799
+ return null;
2800
+ }
2801
+ const content = await response.text();
2802
+ const { data } = matter6(content);
2803
+ if (!data.name || !data.description) {
2804
+ return null;
2805
+ }
2806
+ const files = /* @__PURE__ */ new Map();
2807
+ files.set("SKILL.md", content);
2808
+ const otherFiles = entry.files.filter((f) => f.toLowerCase() !== "skill.md");
2809
+ const filePromises = otherFiles.map(async (filePath) => {
2810
+ try {
2811
+ const fileUrl = `${skillBaseUrl}/${filePath}`;
2812
+ const fileResponse = await fetch(fileUrl);
2813
+ if (fileResponse.ok) {
2814
+ const fileContent = await fileResponse.text();
2815
+ return { path: filePath, content: fileContent };
2816
+ }
2817
+ } catch {
2818
+ return null;
2819
+ }
2820
+ return null;
2821
+ });
2822
+ const fileResults = await Promise.all(filePromises);
2823
+ for (const result of fileResults) {
2824
+ if (result) {
2825
+ files.set(result.path, result.content);
2826
+ }
2827
+ }
2828
+ return {
2829
+ name: data.name,
2830
+ description: data.description,
2831
+ content,
2832
+ installName: entry.name,
2833
+ sourceUrl: skillMdUrl,
2834
+ metadata: data.metadata,
2835
+ files,
2836
+ indexEntry: entry
2837
+ };
2838
+ } catch {
2839
+ return null;
2840
+ }
2841
+ }
2842
+ async fetchAllSkills(url) {
2843
+ try {
2844
+ const result = await this.fetchIndex(url);
2845
+ if (!result) {
2846
+ return [];
2847
+ }
2848
+ const { index, resolvedBaseUrl } = result;
2849
+ const skillPromises = index.skills.map(
2850
+ (entry) => this.fetchSkillByEntry(resolvedBaseUrl, entry)
2851
+ );
2852
+ const results = await Promise.all(skillPromises);
2853
+ return results.filter((skill) => Boolean(skill));
2854
+ } catch {
2855
+ return [];
2856
+ }
2857
+ }
2858
+ toRawUrl(url) {
2859
+ try {
2860
+ const parsed = new URL(url);
2861
+ if (url.toLowerCase().endsWith("/skill.md")) {
2862
+ return url;
2863
+ }
2864
+ const pathMatch = parsed.pathname.match(/\/.well-known\/skills\/([^/]+)\/?$/);
2865
+ if (pathMatch?.[1]) {
2866
+ const basePath2 = parsed.pathname.replace(/\/.well-known\/skills\/.*$/, "");
2867
+ return `${parsed.protocol}//${parsed.host}${basePath2}/${this.WELL_KNOWN_PATH}/${pathMatch[1]}/SKILL.md`;
2868
+ }
2869
+ const basePath = parsed.pathname.replace(/\/$/, "");
2870
+ return `${parsed.protocol}//${parsed.host}${basePath}/${this.WELL_KNOWN_PATH}/${this.INDEX_FILE}`;
2871
+ } catch {
2872
+ return url;
2873
+ }
2874
+ }
2875
+ getSourceIdentifier(url) {
2876
+ try {
2877
+ const parsed = new URL(url);
2878
+ const hostParts = parsed.hostname.split(".");
2879
+ if (hostParts.length >= 2) {
2880
+ const tld = hostParts[hostParts.length - 1];
2881
+ const sld = hostParts[hostParts.length - 2];
2882
+ return `${sld}/${tld}`;
2883
+ }
2884
+ return parsed.hostname.replace(".", "/");
2885
+ } catch {
2886
+ return "unknown/unknown";
2887
+ }
2888
+ }
2889
+ };
2890
+ var wellKnownProvider = new WellKnownProvider();
2891
+
2623
2892
  // src/providers/index.ts
2624
2893
  registerProvider(mintlifyProvider);
2625
2894
  registerProvider(huggingFaceProvider);
2895
+ registerProvider(wellKnownProvider);
2626
2896
  registerProvider(rawSkillProvider);
2627
2897
 
2628
2898
  // src/flows/remote-skill.ts
@@ -2678,10 +2948,56 @@ async function resolveRemoteSkill(url) {
2678
2948
  };
2679
2949
  }
2680
2950
 
2951
+ // src/flows/well-known-skills.ts
2952
+ import { mkdir as mkdir4, mkdtemp as mkdtemp3, writeFile as writeFile3 } from "fs/promises";
2953
+ import { tmpdir as tmpdir4 } from "os";
2954
+ import { dirname as dirname3, join as join11 } from "path";
2955
+ async function prepareWellKnownSkills(sourceUrl) {
2956
+ const discovered = await wellKnownProvider.fetchAllSkills(sourceUrl);
2957
+ if (discovered.length === 0) {
2958
+ throw new Error(
2959
+ "No skills found at this URL. Make sure it exposes /.well-known/skills/index.json."
2960
+ );
2961
+ }
2962
+ const tempDir = await mkdtemp3(join11(tmpdir4(), "playbooks-skill-"));
2963
+ registerTempDir(tempDir);
2964
+ await mkdir4(tempDir, { recursive: true });
2965
+ const originMap = /* @__PURE__ */ new Map();
2966
+ const skills = [];
2967
+ const sourceIdentifier = wellKnownProvider.getSourceIdentifier(sourceUrl);
2968
+ for (const skill of discovered) {
2969
+ const skillDir = join11(tempDir, skill.installName);
2970
+ await mkdir4(skillDir, { recursive: true });
2971
+ for (const [filePath, content] of skill.files.entries()) {
2972
+ const targetPath = join11(skillDir, filePath);
2973
+ if (!isPathSafe(skillDir, targetPath)) {
2974
+ throw new Error("Invalid file path in well-known skill index.");
2975
+ }
2976
+ await mkdir4(dirname3(targetPath), { recursive: true });
2977
+ await writeFile3(targetPath, content, "utf-8");
2978
+ }
2979
+ const localSkill = {
2980
+ name: skill.installName,
2981
+ description: skill.description,
2982
+ path: skillDir,
2983
+ rawContent: skill.content,
2984
+ metadata: skill.metadata
2985
+ };
2986
+ skills.push(localSkill);
2987
+ originMap.set(getSkillDisplayName(localSkill), {
2988
+ sourceType: "well-known",
2989
+ sourceUrl: skill.sourceUrl,
2990
+ source: sourceIdentifier,
2991
+ skillPath: skill.sourceUrl
2992
+ });
2993
+ }
2994
+ return { tempDir, skills, originBySkillName: originMap };
2995
+ }
2996
+
2681
2997
  // src/marketplace.ts
2682
2998
  import { existsSync as existsSync4, statSync } from "fs";
2683
2999
  import { readFile as readFile3 } from "fs/promises";
2684
- import { dirname as dirname3, join as join11, posix } from "path";
3000
+ import { dirname as dirname4, join as join12, posix } from "path";
2685
3001
  function toRecord(value) {
2686
3002
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2687
3003
  return value;
@@ -2752,7 +3068,7 @@ function resolveLocalMarketplacePath(input) {
2752
3068
  return input;
2753
3069
  }
2754
3070
  if (stats.isDirectory()) {
2755
- const candidate = join11(input, ".claude-plugin", "marketplace.json");
3071
+ const candidate = join12(input, ".claude-plugin", "marketplace.json");
2756
3072
  if (existsSync4(candidate)) return candidate;
2757
3073
  }
2758
3074
  return null;
@@ -2769,7 +3085,7 @@ async function loadMarketplace(input, ref) {
2769
3085
  }
2770
3086
  const content = await readFile3(localPath, "utf-8");
2771
3087
  const json = JSON.parse(content);
2772
- return { json, context: { kind: "local", baseDir: dirname3(localPath) } };
3088
+ return { json, context: { kind: "local", baseDir: dirname4(localPath) } };
2773
3089
  }
2774
3090
  if (isOwnerRepoShorthand(input)) {
2775
3091
  const [owner, repo] = input.split("/");
@@ -2881,8 +3197,8 @@ function resolvePluginSource(plugin, context) {
2881
3197
  const src = plugin.source;
2882
3198
  if (typeof src === "string") {
2883
3199
  if (context.kind === "local" && context.baseDir) {
2884
- const base = join11(context.baseDir, pluginRoot);
2885
- return { kind: "local", localDir: join11(base, src), overrides };
3200
+ const base = join12(context.baseDir, pluginRoot);
3201
+ return { kind: "local", localDir: join12(base, src), overrides };
2886
3202
  }
2887
3203
  if (context.kind === "github" && context.gh) {
2888
3204
  const basePath = posix.join(context.gh.basePath || "", pluginRoot || "");
@@ -3056,6 +3372,34 @@ function MultiSelect({
3056
3372
  ] });
3057
3373
  }
3058
3374
 
3375
+ // src/tui/utils/skill-selection.ts
3376
+ import { basename as basename3 } from "path";
3377
+ function matchesSkillName(skill, input) {
3378
+ const normalized = input.toLowerCase();
3379
+ const byName = skill.name.toLowerCase() === normalized;
3380
+ const byPath = basename3(skill.path).toLowerCase() === normalized;
3381
+ return byName || byPath;
3382
+ }
3383
+ function autoSelect(skills, options) {
3384
+ if (options.skill && options.skill.length > 0) {
3385
+ const selected = skills.filter((s) => options.skill?.some((name) => matchesSkillName(s, name)));
3386
+ if (selected.length === 0) {
3387
+ return {
3388
+ status: "prompt",
3389
+ message: `No matching skills found for: ${options.skill.join(", ")}`
3390
+ };
3391
+ }
3392
+ return { status: "selected", skills: selected };
3393
+ }
3394
+ if (skills.length === 1) {
3395
+ return { status: "selected", skills };
3396
+ }
3397
+ if (options.yes) {
3398
+ return { status: "selected", skills };
3399
+ }
3400
+ return { status: "prompt" };
3401
+ }
3402
+
3059
3403
  // src/tui/screens/AddSkillSelect.tsx
3060
3404
  import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
3061
3405
  function AddSkillSelectScreen() {
@@ -3113,11 +3457,11 @@ function AddSkillSelectScreen() {
3113
3457
  if (!resolved) {
3114
3458
  throw new Error("Unable to fetch SKILL.md from that URL.");
3115
3459
  }
3116
- const tempDir2 = await mkdtemp3(join12(tmpdir4(), "playbooks-skill-"));
3460
+ const tempDir2 = await mkdtemp4(join13(tmpdir5(), "playbooks-skill-"));
3117
3461
  registerTempDir(tempDir2);
3118
3462
  tempDirForCleanup = tempDir2;
3119
- await mkdir4(tempDir2, { recursive: true });
3120
- await writeFile3(join12(tempDir2, "SKILL.md"), resolved.remoteSkill.content, "utf-8");
3463
+ await mkdir5(tempDir2, { recursive: true });
3464
+ await writeFile4(join13(tempDir2, "SKILL.md"), resolved.remoteSkill.content, "utf-8");
3121
3465
  const skill = {
3122
3466
  name: resolved.remoteSkill.installName,
3123
3467
  description: resolved.remoteSkill.description,
@@ -3143,6 +3487,38 @@ function AddSkillSelectScreen() {
3143
3487
  navigateTo("add-targets");
3144
3488
  return;
3145
3489
  }
3490
+ if (parsed.type === "well-known") {
3491
+ const prepared = await prepareWellKnownSkills(parsed.url);
3492
+ tempDirForCleanup = prepared.tempDir;
3493
+ updateAddSkill({
3494
+ parsed,
3495
+ tempDir: prepared.tempDir,
3496
+ skills: prepared.skills,
3497
+ originBySkillName: prepared.originBySkillName
3498
+ });
3499
+ if (options.list) {
3500
+ keepTempDir = true;
3501
+ setListMode(true);
3502
+ setStatus("list");
3503
+ return;
3504
+ }
3505
+ const autoSelection2 = autoSelect(prepared.skills, options);
3506
+ if (autoSelection2.status === "selected") {
3507
+ keepTempDir = true;
3508
+ updateAddSkill({ selectedSkills: autoSelection2.skills });
3509
+ navigateTo("add-targets");
3510
+ return;
3511
+ }
3512
+ if (autoSelection2.status === "error") {
3513
+ throw new Error(autoSelection2.message);
3514
+ }
3515
+ if (autoSelection2.status === "prompt" && autoSelection2.message) {
3516
+ setFlash(autoSelection2.message);
3517
+ }
3518
+ keepTempDir = true;
3519
+ setStatus("ready");
3520
+ return;
3521
+ }
3146
3522
  let skillsDir = "";
3147
3523
  let tempDir = null;
3148
3524
  if (parsed.type === "local") {
@@ -3317,31 +3693,6 @@ function AddSkillSelectScreen() {
3317
3693
  )
3318
3694
  ] });
3319
3695
  }
3320
- function matchesSkillName(skill, input) {
3321
- const normalized = input.toLowerCase();
3322
- const byName = skill.name.toLowerCase() === normalized;
3323
- const byPath = basename3(skill.path).toLowerCase() === normalized;
3324
- return byName || byPath;
3325
- }
3326
- function autoSelect(skills, options) {
3327
- if (options.skill && options.skill.length > 0) {
3328
- const selected = skills.filter((s) => options.skill?.some((name) => matchesSkillName(s, name)));
3329
- if (selected.length === 0) {
3330
- return {
3331
- status: "prompt",
3332
- message: `No matching skills found for: ${options.skill.join(", ")}`
3333
- };
3334
- }
3335
- return { status: "selected", skills: selected };
3336
- }
3337
- if (skills.length === 1) {
3338
- return { status: "selected", skills };
3339
- }
3340
- if (options.yes) {
3341
- return { status: "selected", skills };
3342
- }
3343
- return { status: "prompt" };
3344
- }
3345
3696
 
3346
3697
  // src/tui/screens/AddSource.tsx
3347
3698
  import { Box as Box16, Text as Text14 } from "ink";
@@ -3431,10 +3782,11 @@ function AddSourceScreen() {
3431
3782
  // src/tui/screens/AddTargets.tsx
3432
3783
  import { Box as Box17, Text as Text15 } from "ink";
3433
3784
  import React14 from "react";
3434
- import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
3785
+ import { Fragment, jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
3435
3786
  function AddTargetsScreen() {
3436
3787
  const { invocation, addSkill, updateAddSkill, navigateTo, setFlash, navAction } = useNavigation();
3437
3788
  const [status, setStatus] = React14.useState("loading");
3789
+ const [mode, setMode] = React14.useState("choice");
3438
3790
  const [availableAgents, setAvailableAgents] = React14.useState([]);
3439
3791
  const [showLoading, setShowLoading] = React14.useState(false);
3440
3792
  const spinner = useSpinnerFrame(status === "loading");
@@ -3447,6 +3799,7 @@ function AddTargetsScreen() {
3447
3799
  const list = installed.length > 0 ? installed : Object.keys(agents);
3448
3800
  setAvailableAgents(list);
3449
3801
  setStatus("ready");
3802
+ setMode("choice");
3450
3803
  if (navAction !== "pop" && addSkill.targetAgents && addSkill.targetAgents.length > 0) {
3451
3804
  navigateTo("add-scope");
3452
3805
  return;
@@ -3505,7 +3858,40 @@ function AddTargetsScreen() {
3505
3858
  value: agent,
3506
3859
  label: agents[agent].displayName
3507
3860
  }));
3508
- return /* @__PURE__ */ jsxs15(Box17, { flexDirection: "column", padding: 1, children: [
3861
+ return /* @__PURE__ */ jsx19(Box17, { flexDirection: "column", padding: 1, children: mode === "choice" ? /* @__PURE__ */ jsxs15(Fragment, { children: [
3862
+ /* @__PURE__ */ jsx19(AddFlowHeader, { title: "Install to" }),
3863
+ /* @__PURE__ */ jsx19(
3864
+ SingleSelect,
3865
+ {
3866
+ items: [
3867
+ {
3868
+ label: "All detected agents (Recommended)",
3869
+ value: "all",
3870
+ hint: `Install to all ${availableAgents.length} detected agents`
3871
+ },
3872
+ {
3873
+ label: "Select specific agents",
3874
+ value: "select",
3875
+ hint: "Choose a subset of detected agents"
3876
+ }
3877
+ ],
3878
+ initialValue: "all",
3879
+ onSubmit: (value) => {
3880
+ if (value === "all") {
3881
+ updateAddSkill({
3882
+ targetAgents: availableAgents,
3883
+ installGlobally: void 0,
3884
+ installMode: void 0,
3885
+ planLines: void 0
3886
+ });
3887
+ navigateTo("add-scope");
3888
+ return;
3889
+ }
3890
+ setMode("select");
3891
+ }
3892
+ }
3893
+ )
3894
+ ] }) : /* @__PURE__ */ jsxs15(Fragment, { children: [
3509
3895
  /* @__PURE__ */ jsx19(AddFlowHeader, { title: "Select agents" }),
3510
3896
  /* @__PURE__ */ jsx19(
3511
3897
  MultiSelect,
@@ -3527,7 +3913,7 @@ function AddTargetsScreen() {
3527
3913
  }
3528
3914
  }
3529
3915
  )
3530
- ] });
3916
+ ] }) });
3531
3917
  }
3532
3918
 
3533
3919
  // src/tui/screens/FindSkillResults.tsx
@@ -3796,16 +4182,16 @@ import React18 from "react";
3796
4182
 
3797
4183
  // src/installed-skills.ts
3798
4184
  import { lstat as lstat2, readFile as readFile4, readdir as readdir3, stat as stat2 } from "fs/promises";
3799
- import { basename as basename4, join as join13 } from "path";
3800
- import matter6 from "gray-matter";
4185
+ import { basename as basename4, join as join14 } from "path";
4186
+ import matter7 from "gray-matter";
3801
4187
  function getAgentSkillsDir(agent, scope, cwd) {
3802
4188
  const config = agents[agent];
3803
- return scope === "global" ? config.globalSkillsDir : join13(cwd, config.skillsDir);
4189
+ return scope === "global" ? config.globalSkillsDir : join14(cwd, config.skillsDir);
3804
4190
  }
3805
4191
  async function readSkillFrontmatter(skillMdPath) {
3806
4192
  try {
3807
4193
  const content = await readFile4(skillMdPath, "utf-8");
3808
- const { data } = matter6(content);
4194
+ const { data } = matter7(content);
3809
4195
  return {
3810
4196
  name: typeof data.name === "string" ? data.name : void 0,
3811
4197
  description: typeof data.description === "string" ? data.description : void 0
@@ -3824,7 +4210,7 @@ async function listSkillsForAgent(agent, scope, cwd = process.cwd()) {
3824
4210
  continue;
3825
4211
  }
3826
4212
  const slug = entry.name;
3827
- const skillDir = join13(dir, slug);
4213
+ const skillDir = join14(dir, slug);
3828
4214
  const isSymlink = entry.isSymbolicLink();
3829
4215
  let isBroken = false;
3830
4216
  if (isSymlink) {
@@ -3834,7 +4220,7 @@ async function listSkillsForAgent(agent, scope, cwd = process.cwd()) {
3834
4220
  isBroken = true;
3835
4221
  }
3836
4222
  }
3837
- const skillMdPath = join13(skillDir, "SKILL.md");
4223
+ const skillMdPath = join14(skillDir, "SKILL.md");
3838
4224
  if (isBroken) {
3839
4225
  skills.push({
3840
4226
  slug,
@@ -3877,7 +4263,7 @@ async function findSkillInstallations(skillName, scope, cwd = process.cwd()) {
3877
4263
  }
3878
4264
  for (const agent of Object.keys(agents)) {
3879
4265
  const baseDir = getAgentSkillsDir(agent, scope, cwd);
3880
- const skillDir = join13(baseDir, sanitized);
4266
+ const skillDir = join14(baseDir, sanitized);
3881
4267
  try {
3882
4268
  const stats = await lstat2(skillDir);
3883
4269
  installs.push({
@@ -4291,14 +4677,14 @@ import { Box as Box25, Text as Text22 } from "ink";
4291
4677
  import React21 from "react";
4292
4678
 
4293
4679
  // src/flows/marketplace.ts
4294
- import { dirname as dirname4, join as join14, relative as relative2 } from "path";
4680
+ import { dirname as dirname5, join as join15, relative as relative2 } from "path";
4295
4681
  function normalizeCandidatePath(basePath, candidate) {
4296
4682
  if (!candidate) return basePath;
4297
4683
  const cleaned = candidate.replace(/\\/g, "/");
4298
4684
  if (cleaned.endsWith(".md")) {
4299
- return join14(basePath, dirname4(cleaned));
4685
+ return join15(basePath, dirname5(cleaned));
4300
4686
  }
4301
- return join14(basePath, cleaned);
4687
+ return join15(basePath, cleaned);
4302
4688
  }
4303
4689
  function collectOverridePaths(overrides) {
4304
4690
  const paths = ["skills", "commands", "agents", "hooks"];
@@ -4375,7 +4761,7 @@ async function collectMarketplaceSkills(plugins, context) {
4375
4761
  const key = `github:${owner}/${repo}@${ref}`;
4376
4762
  const repoUrl = `https://github.com/${owner}/${repo}.git`;
4377
4763
  const { tempDir } = await ensureRepoClone(key, repoUrl, ref);
4378
- const repoRoot = join14(tempDir, path || "");
4764
+ const repoRoot = join15(tempDir, path || "");
4379
4765
  const baseDir = normalizeCandidatePath(repoRoot, plugin.pluginRoot || "");
4380
4766
  const overridePaths = collectOverridePaths(resolved.overrides);
4381
4767
  const scanned = /* @__PURE__ */ new Set();
@@ -4407,7 +4793,7 @@ async function collectMarketplaceSkills(plugins, context) {
4407
4793
  const key = `gitlab:${namespacePath}/${repo}@${ref}`;
4408
4794
  const repoUrl = `https://gitlab.com/${namespacePath}/${repo}.git`;
4409
4795
  const { tempDir } = await ensureRepoClone(key, repoUrl, ref);
4410
- const repoRoot = join14(tempDir, path || "");
4796
+ const repoRoot = join15(tempDir, path || "");
4411
4797
  const baseDir = normalizeCandidatePath(repoRoot, plugin.pluginRoot || "");
4412
4798
  const overridePaths = collectOverridePaths(resolved.overrides);
4413
4799
  const scanned = /* @__PURE__ */ new Set();
@@ -4594,9 +4980,9 @@ import { Box as Box26, Text as Text23, useInput as useInput5 } from "ink";
4594
4980
  import React22 from "react";
4595
4981
 
4596
4982
  // src/flows/update-skills.ts
4597
- import { mkdir as mkdir5, mkdtemp as mkdtemp4, rm as rm6, stat as stat3, writeFile as writeFile4 } from "fs/promises";
4598
- import { tmpdir as tmpdir5 } from "os";
4599
- import { dirname as dirname5, join as join15 } from "path";
4983
+ import { mkdir as mkdir6, mkdtemp as mkdtemp5, rm as rm6, stat as stat3, writeFile as writeFile5 } from "fs/promises";
4984
+ import { tmpdir as tmpdir6 } from "os";
4985
+ import { dirname as dirname6, join as join16 } from "path";
4600
4986
  var repoTreeCache = /* @__PURE__ */ new Map();
4601
4987
  function normalizeSkillFolderPath(skillPath) {
4602
4988
  let folderPath = skillPath;
@@ -4691,8 +5077,8 @@ async function updateFromRepo(target) {
4691
5077
  let sourceDir = null;
4692
5078
  if (target.entry.skillPath) {
4693
5079
  const normalizedPath = target.entry.skillPath.replace(/\\/g, "/");
4694
- const skillDir = join15(tempDir, dirname5(normalizedPath));
4695
- if (await pathExists(join15(skillDir, "SKILL.md"))) {
5080
+ const skillDir = join16(tempDir, dirname6(normalizedPath));
5081
+ if (await pathExists(join16(skillDir, "SKILL.md"))) {
4696
5082
  sourceDir = skillDir;
4697
5083
  }
4698
5084
  }
@@ -4714,10 +5100,15 @@ async function updateFromRepo(target) {
4714
5100
  async function updateFromRemote(target) {
4715
5101
  const provider = findProvider(target.entry.sourceUrl);
4716
5102
  let content = null;
5103
+ let files = null;
4717
5104
  if (provider) {
4718
5105
  const skill = await provider.fetchSkill(target.entry.sourceUrl);
4719
5106
  if (skill) {
4720
- content = skill.content;
5107
+ if ("files" in skill && skill.files instanceof Map) {
5108
+ files = skill.files;
5109
+ } else {
5110
+ content = skill.content;
5111
+ }
4721
5112
  }
4722
5113
  } else if (target.entry.sourceType === "mintlify") {
4723
5114
  const legacy = await fetchMintlifySkill(target.entry.sourceUrl);
@@ -4725,14 +5116,25 @@ async function updateFromRemote(target) {
4725
5116
  content = legacy.content;
4726
5117
  }
4727
5118
  }
4728
- if (!content) {
5119
+ if (!content && !files) {
4729
5120
  return false;
4730
5121
  }
4731
- const tempDir = await mkdtemp4(join15(tmpdir5(), "playbooks-skill-"));
5122
+ const tempDir = await mkdtemp5(join16(tmpdir6(), "playbooks-skill-"));
4732
5123
  registerTempDir(tempDir);
4733
5124
  try {
4734
- await mkdir5(tempDir, { recursive: true });
4735
- await writeFile4(join15(tempDir, "SKILL.md"), content, "utf-8");
5125
+ await mkdir6(tempDir, { recursive: true });
5126
+ if (files) {
5127
+ for (const [filePath, fileContent] of files.entries()) {
5128
+ const targetPath = join16(tempDir, filePath);
5129
+ if (!isPathSafe(tempDir, targetPath)) {
5130
+ continue;
5131
+ }
5132
+ await mkdir6(dirname6(targetPath), { recursive: true });
5133
+ await writeFile5(targetPath, fileContent, "utf-8");
5134
+ }
5135
+ } else if (content) {
5136
+ await writeFile5(join16(tempDir, "SKILL.md"), content, "utf-8");
5137
+ }
4736
5138
  return await applyUpdateFromDir(target.name, target.scope, tempDir);
4737
5139
  } finally {
4738
5140
  await cleanupTempDir(tempDir);
@@ -5003,7 +5405,7 @@ function sortTargets(a, b) {
5003
5405
  }
5004
5406
 
5005
5407
  // src/tui/ScreenRouter.tsx
5006
- import { Fragment, jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
5408
+ import { Fragment as Fragment2, jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
5007
5409
  function ScreenRouter() {
5008
5410
  const { screen } = useNavigation();
5009
5411
  const render2 = (s) => {
@@ -5046,7 +5448,7 @@ function ScreenRouter() {
5046
5448
  return null;
5047
5449
  }
5048
5450
  };
5049
- return /* @__PURE__ */ jsxs24(Fragment, { children: [
5451
+ return /* @__PURE__ */ jsxs24(Fragment2, { children: [
5050
5452
  /* @__PURE__ */ jsx29(BrandHeader, {}),
5051
5453
  render2(screen),
5052
5454
  /* @__PURE__ */ jsx29(FlashBar, { align: "center" })
@@ -5145,7 +5547,7 @@ function runApp(initialInvocation, initialScreen) {
5145
5547
  var version = package_default.version;
5146
5548
  setVersion(version);
5147
5549
  setupTempDirCleanup();
5148
- program.name("playbooks").description("Playbooks CLI").version(version);
5550
+ program.name("playbooks").description("playbooks CLI").version(version);
5149
5551
  program.addHelpCommand();
5150
5552
  var applyAddSkillOptions = (cmd) => cmd.option("-g, --global", "Install globally (user-level) instead of project-level").option(
5151
5553
  "-a, --agent <agents...>",
@@ -5283,6 +5685,36 @@ program.command("get <url> [outKeyword] [outPath]").description("Fetch a URL as
5283
5685
  }
5284
5686
  outputPath = outPath;
5285
5687
  }
5688
+ const shouldUseTui = Boolean(process.stdout.isTTY) && !outputPath;
5689
+ if (!shouldUseTui) {
5690
+ try {
5691
+ const data = await fetchUrlMarkdown(url);
5692
+ if (outputPath) {
5693
+ if (options.json) {
5694
+ writeFileSync(outputPath, `${JSON.stringify(data, null, 2)}
5695
+ `, "utf8");
5696
+ return;
5697
+ }
5698
+ const body3 = data.markdown.endsWith("\n") ? data.markdown : `${data.markdown}
5699
+ `;
5700
+ writeFileSync(outputPath, body3, "utf8");
5701
+ return;
5702
+ }
5703
+ if (options.json) {
5704
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
5705
+ `);
5706
+ return;
5707
+ }
5708
+ const body2 = data.markdown.endsWith("\n") ? data.markdown : `${data.markdown}
5709
+ `;
5710
+ process.stdout.write(body2);
5711
+ return;
5712
+ } catch (error) {
5713
+ const message = error instanceof Error ? error.message : "Failed to fetch markdown.";
5714
+ console.error(`Failed to fetch markdown: ${message}`);
5715
+ process.exit(1);
5716
+ }
5717
+ }
5286
5718
  await runApp(
5287
5719
  { intent: "get-url", source: url, options: { json: options.json, output: outputPath } },
5288
5720
  "get-url"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playbooks",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Install agent skills, MCPs and docs into your coding agents from any git repository.",
5
5
  "type": "module",
6
6
  "bin": {