playbooks 0.1.2 → 0.1.4

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 +116 -48
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { program } from "commander";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "playbooks",
10
- version: "0.1.2",
10
+ version: "0.1.4",
11
11
  description: "Install agent skills, MCPs and docs into your coding agents from any git repository.",
12
12
  type: "module",
13
13
  bin: {
@@ -950,12 +950,42 @@ async function isSkillInstalled(skillName, agentType, options = {}) {
950
950
  }
951
951
 
952
952
  // src/skills.ts
953
+ import { existsSync as existsSync2 } from "fs";
953
954
  import { readFile, readdir as readdir2, stat } from "fs/promises";
954
955
  import { basename as basename2, dirname, join as join6 } from "path";
955
956
  import matter from "gray-matter";
956
- var SKIP_DIRS = ["node_modules", ".git", "dist", "build", "__pycache__"];
957
+ var SKIP_DIRS = ["node_modules", ".git", ".github", "dist", "build", "__pycache__"];
958
+ var DENIED_SEGMENTS = /* @__PURE__ */ new Set([
959
+ ".git",
960
+ "node_modules",
961
+ ".github",
962
+ "playbooks",
963
+ "context",
964
+ "prompts",
965
+ "backups",
966
+ "backup",
967
+ "dist",
968
+ "deprecated"
969
+ ]);
970
+ var normalizePath = (value) => value.replace(/^\/+/, "");
971
+ var normalizeRoot = (value) => normalizePath(value).replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
972
+ var isDeniedPath = (path) => {
973
+ const cleaned = normalizePath(path);
974
+ const segments = cleaned.split("/").map((segment) => segment.toLowerCase());
975
+ for (const segment of segments) {
976
+ if (!segment) continue;
977
+ if (segment === ".claude-plugin") {
978
+ continue;
979
+ }
980
+ if (DENIED_SEGMENTS.has(segment)) {
981
+ return true;
982
+ }
983
+ }
984
+ return false;
985
+ };
957
986
  async function hasSkillMd(dir) {
958
987
  try {
988
+ if (isDeniedPath(dir)) return false;
959
989
  const skillPath = join6(dir, "SKILL.md");
960
990
  const stats = await stat(skillPath);
961
991
  return stats.isFile();
@@ -965,6 +995,7 @@ async function hasSkillMd(dir) {
965
995
  }
966
996
  async function parseSkillMd(skillMdPath) {
967
997
  try {
998
+ if (isDeniedPath(skillMdPath)) return null;
968
999
  const content = await readFile(skillMdPath, "utf-8");
969
1000
  const { data } = matter(content);
970
1001
  if (!data.name || !data.description) {
@@ -984,6 +1015,7 @@ async function parseSkillMd(skillMdPath) {
984
1015
  async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
985
1016
  const skillDirs = [];
986
1017
  if (depth > maxDepth) return skillDirs;
1018
+ if (isDeniedPath(dir)) return skillDirs;
987
1019
  try {
988
1020
  if (await hasSkillMd(dir)) {
989
1021
  skillDirs.push(dir);
@@ -999,9 +1031,51 @@ async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
999
1031
  }
1000
1032
  return skillDirs;
1001
1033
  }
1034
+ async function readMarketplacePluginRoots(basePath) {
1035
+ const filePath = join6(basePath, ".claude-plugin", "marketplace.json");
1036
+ if (!existsSync2(filePath)) return [];
1037
+ try {
1038
+ const raw = await readFile(filePath, "utf-8");
1039
+ const parsed = JSON.parse(raw);
1040
+ const roots = (parsed.plugins ?? []).map((plugin) => typeof plugin.source === "string" ? plugin.source : null).filter(Boolean);
1041
+ return roots.map((root) => normalizeRoot(root)).filter(Boolean);
1042
+ } catch {
1043
+ return [];
1044
+ }
1045
+ }
1046
+ async function collectSkillsFromRoot(root, seenSlugs, skills) {
1047
+ if (!root || !existsSync2(root) || isDeniedPath(root)) return;
1048
+ const skillDirs = await findSkillDirs(root);
1049
+ for (const skillDir of skillDirs) {
1050
+ const skill = await parseSkillMd(join6(skillDir, "SKILL.md"));
1051
+ if (!skill) continue;
1052
+ const slug = basename2(skill.path).toLowerCase();
1053
+ if (seenSlugs.has(slug)) continue;
1054
+ skills.push(skill);
1055
+ seenSlugs.add(slug);
1056
+ }
1057
+ }
1058
+ async function listPluginSkillRoots(basePath) {
1059
+ const pluginsDir = join6(basePath, "plugins");
1060
+ if (!existsSync2(pluginsDir)) return [];
1061
+ try {
1062
+ const entries = await readdir2(pluginsDir, { withFileTypes: true });
1063
+ const roots = [];
1064
+ for (const entry of entries) {
1065
+ if (!entry.isDirectory()) continue;
1066
+ const candidate = join6(pluginsDir, entry.name, "skills");
1067
+ if (existsSync2(candidate)) {
1068
+ roots.push(candidate);
1069
+ }
1070
+ }
1071
+ return roots;
1072
+ } catch {
1073
+ return [];
1074
+ }
1075
+ }
1002
1076
  async function discoverSkills(basePath, subpath) {
1003
1077
  const skills = [];
1004
- const seenNames = /* @__PURE__ */ new Set();
1078
+ const seenSlugs = /* @__PURE__ */ new Set();
1005
1079
  const searchPath = subpath ? join6(basePath, subpath) : basePath;
1006
1080
  if (await hasSkillMd(searchPath)) {
1007
1081
  const skill = await parseSkillMd(join6(searchPath, "SKILL.md"));
@@ -1010,17 +1084,21 @@ async function discoverSkills(basePath, subpath) {
1010
1084
  return skills;
1011
1085
  }
1012
1086
  }
1013
- const prioritySearchDirs = [
1014
- searchPath,
1015
- join6(searchPath, "skills"),
1016
- join6(searchPath, "skills/.curated"),
1017
- join6(searchPath, "skills/.experimental"),
1018
- join6(searchPath, "skills/.system"),
1087
+ const marketplaceRoots = await readMarketplacePluginRoots(searchPath);
1088
+ for (const root of marketplaceRoots) {
1089
+ const skillsRoot = root.toLowerCase().endsWith("/skills") ? root : `${root}/skills`;
1090
+ await collectSkillsFromRoot(join6(searchPath, skillsRoot), seenSlugs, skills);
1091
+ }
1092
+ await collectSkillsFromRoot(join6(searchPath, "skills"), seenSlugs, skills);
1093
+ const pluginRoots = await listPluginSkillRoots(searchPath);
1094
+ for (const root of pluginRoots) {
1095
+ await collectSkillsFromRoot(root, seenSlugs, skills);
1096
+ }
1097
+ await collectSkillsFromRoot(join6(searchPath, ".claude-plugin"), seenSlugs, skills);
1098
+ const agentRoots = [
1019
1099
  join6(searchPath, ".agent/skills"),
1020
1100
  join6(searchPath, ".agents/skills"),
1021
- join6(searchPath, ".claude/skills"),
1022
1101
  join6(searchPath, ".cline/skills"),
1023
- join6(searchPath, ".codex/skills"),
1024
1102
  join6(searchPath, ".commandcode/skills"),
1025
1103
  join6(searchPath, ".continue/skills"),
1026
1104
  join6(searchPath, ".cursor/skills"),
@@ -1030,7 +1108,6 @@ async function discoverSkills(basePath, subpath) {
1030
1108
  join6(searchPath, ".kilocode/skills"),
1031
1109
  join6(searchPath, ".kiro/skills"),
1032
1110
  join6(searchPath, ".neovate/skills"),
1033
- join6(searchPath, ".opencode/skills"),
1034
1111
  join6(searchPath, ".openhands/skills"),
1035
1112
  join6(searchPath, ".pi/skills"),
1036
1113
  join6(searchPath, ".qoder/skills"),
@@ -1039,32 +1116,18 @@ async function discoverSkills(basePath, subpath) {
1039
1116
  join6(searchPath, ".windsurf/skills"),
1040
1117
  join6(searchPath, ".zencoder/skills")
1041
1118
  ];
1042
- for (const dir of prioritySearchDirs) {
1043
- try {
1044
- const entries = await readdir2(dir, { withFileTypes: true });
1045
- for (const entry of entries) {
1046
- if (entry.isDirectory()) {
1047
- const skillDir = join6(dir, entry.name);
1048
- if (await hasSkillMd(skillDir)) {
1049
- const skill = await parseSkillMd(join6(skillDir, "SKILL.md"));
1050
- if (skill && !seenNames.has(skill.name)) {
1051
- skills.push(skill);
1052
- seenNames.add(skill.name);
1053
- }
1054
- }
1055
- }
1056
- }
1057
- } catch {
1058
- }
1119
+ for (const root of agentRoots) {
1120
+ await collectSkillsFromRoot(root, seenSlugs, skills);
1059
1121
  }
1060
1122
  if (skills.length === 0) {
1061
1123
  const allSkillDirs = await findSkillDirs(searchPath);
1062
1124
  for (const skillDir of allSkillDirs) {
1063
1125
  const skill = await parseSkillMd(join6(skillDir, "SKILL.md"));
1064
- if (skill && !seenNames.has(skill.name)) {
1065
- skills.push(skill);
1066
- seenNames.add(skill.name);
1067
- }
1126
+ if (!skill) continue;
1127
+ const slug = basename2(skill.path).toLowerCase();
1128
+ if (seenSlugs.has(slug)) continue;
1129
+ skills.push(skill);
1130
+ seenSlugs.add(slug);
1068
1131
  }
1069
1132
  }
1070
1133
  return skills;
@@ -2034,10 +2097,10 @@ function AddScopeScreen() {
2034
2097
  }
2035
2098
 
2036
2099
  // src/tui/screens/AddSkillSelect.tsx
2037
- import { existsSync as existsSync3 } from "fs";
2100
+ import { existsSync as existsSync4 } from "fs";
2038
2101
  import { mkdir as mkdir4, mkdtemp as mkdtemp2, writeFile as writeFile3 } from "fs/promises";
2039
2102
  import { tmpdir as tmpdir3 } from "os";
2040
- import { join as join11 } from "path";
2103
+ import { basename as basename3, join as join11 } from "path";
2041
2104
  import { Box as Box15, Text as Text13 } from "ink";
2042
2105
  import React11 from "react";
2043
2106
 
@@ -2351,7 +2414,7 @@ async function resolveRemoteSkill(url) {
2351
2414
  }
2352
2415
 
2353
2416
  // src/marketplace.ts
2354
- import { existsSync as existsSync2, statSync } from "fs";
2417
+ import { existsSync as existsSync3, statSync } from "fs";
2355
2418
  import { readFile as readFile3 } from "fs/promises";
2356
2419
  import { dirname as dirname3, join as join10, posix } from "path";
2357
2420
  function toRecord(value) {
@@ -2418,14 +2481,14 @@ function isMarketplaceInput(input) {
2418
2481
  return input.toLowerCase().endsWith("marketplace.json");
2419
2482
  }
2420
2483
  function resolveLocalMarketplacePath(input) {
2421
- if (!existsSync2(input)) return null;
2484
+ if (!existsSync3(input)) return null;
2422
2485
  const stats = statSync(input);
2423
2486
  if (stats.isFile() && input.toLowerCase().endsWith("marketplace.json")) {
2424
2487
  return input;
2425
2488
  }
2426
2489
  if (stats.isDirectory()) {
2427
2490
  const candidate = join10(input, ".claude-plugin", "marketplace.json");
2428
- if (existsSync2(candidate)) return candidate;
2491
+ if (existsSync3(candidate)) return candidate;
2429
2492
  }
2430
2493
  return null;
2431
2494
  }
@@ -2799,7 +2862,7 @@ function AddSkillSelectScreen() {
2799
2862
  throw new Error("Local path is missing.");
2800
2863
  }
2801
2864
  skillsDir = parsed.localPath;
2802
- if (!existsSync3(skillsDir)) {
2865
+ if (!existsSync4(skillsDir)) {
2803
2866
  throw new Error(`Local path does not exist: ${skillsDir}`);
2804
2867
  }
2805
2868
  } else {
@@ -2843,6 +2906,9 @@ function AddSkillSelectScreen() {
2843
2906
  if (autoSelection.status === "error") {
2844
2907
  throw new Error(autoSelection.message);
2845
2908
  }
2909
+ if (autoSelection.status === "prompt" && autoSelection.message) {
2910
+ setFlash(autoSelection.message);
2911
+ }
2846
2912
  if (tempDir) {
2847
2913
  keepTempDir = true;
2848
2914
  }
@@ -2864,7 +2930,7 @@ function AddSkillSelectScreen() {
2864
2930
  return () => {
2865
2931
  cancelled = true;
2866
2932
  };
2867
- }, [source, addSkill.skills, updateAddSkill, navigateTo, options]);
2933
+ }, [source, addSkill.skills, updateAddSkill, navigateTo, options, setFlash]);
2868
2934
  React11.useEffect(() => {
2869
2935
  if (invocation.source) {
2870
2936
  setBackHandler(() => {
@@ -2963,16 +3029,18 @@ function AddSkillSelectScreen() {
2963
3029
  )
2964
3030
  ] });
2965
3031
  }
3032
+ function matchesSkillName(skill, input) {
3033
+ const normalized = input.toLowerCase();
3034
+ const byName = skill.name.toLowerCase() === normalized;
3035
+ const byPath = basename3(skill.path).toLowerCase() === normalized;
3036
+ return byName || byPath;
3037
+ }
2966
3038
  function autoSelect(skills, options) {
2967
3039
  if (options.skill && options.skill.length > 0) {
2968
- const selected = skills.filter(
2969
- (s) => options.skill?.some(
2970
- (name) => s.name.toLowerCase() === name.toLowerCase() || getSkillDisplayName(s).toLowerCase() === name.toLowerCase()
2971
- )
2972
- );
3040
+ const selected = skills.filter((s) => options.skill?.some((name) => matchesSkillName(s, name)));
2973
3041
  if (selected.length === 0) {
2974
3042
  return {
2975
- status: "error",
3043
+ status: "prompt",
2976
3044
  message: `No matching skills found for: ${options.skill.join(", ")}`
2977
3045
  };
2978
3046
  }
@@ -3140,7 +3208,7 @@ import React14 from "react";
3140
3208
 
3141
3209
  // src/installed-skills.ts
3142
3210
  import { lstat as lstat2, readFile as readFile4, readdir as readdir3, stat as stat2 } from "fs/promises";
3143
- import { basename as basename3, join as join12 } from "path";
3211
+ import { basename as basename4, join as join12 } from "path";
3144
3212
  import matter6 from "gray-matter";
3145
3213
  function getAgentSkillsDir(agent, scope, cwd) {
3146
3214
  const config = agents[agent];
@@ -3215,7 +3283,7 @@ async function listSkillsForAgent(agent, scope, cwd = process.cwd()) {
3215
3283
  }
3216
3284
  async function findSkillInstallations(skillName, scope, cwd = process.cwd()) {
3217
3285
  const installs = [];
3218
- const sanitized = basename3(getCanonicalPath(skillName, { global: scope === "global", cwd }));
3286
+ const sanitized = basename4(getCanonicalPath(skillName, { global: scope === "global", cwd }));
3219
3287
  if (!sanitized) {
3220
3288
  return installs;
3221
3289
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playbooks",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Install agent skills, MCPs and docs into your coding agents from any git repository.",
5
5
  "type": "module",
6
6
  "bin": {