playbooks 0.1.2 → 0.1.3

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 +121 -47
  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.3",
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,47 @@ 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
+ ".codex",
963
+ "playbooks",
964
+ "context",
965
+ "prompts",
966
+ "backups",
967
+ "backup",
968
+ "dist",
969
+ "deprecated",
970
+ ".opencode"
971
+ ]);
972
+ var normalizePath = (value) => value.replace(/^\/+/, "");
973
+ var normalizeRoot = (value) => normalizePath(value).replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
974
+ var isDeniedPath = (path) => {
975
+ const cleaned = normalizePath(path);
976
+ const segments = cleaned.split("/").map((segment) => segment.toLowerCase());
977
+ for (const segment of segments) {
978
+ if (!segment) continue;
979
+ if (segment === ".claude-plugin") {
980
+ continue;
981
+ }
982
+ if (segment.startsWith(".claude")) {
983
+ return true;
984
+ }
985
+ if (DENIED_SEGMENTS.has(segment)) {
986
+ return true;
987
+ }
988
+ }
989
+ return false;
990
+ };
957
991
  async function hasSkillMd(dir) {
958
992
  try {
993
+ if (isDeniedPath(dir)) return false;
959
994
  const skillPath = join6(dir, "SKILL.md");
960
995
  const stats = await stat(skillPath);
961
996
  return stats.isFile();
@@ -965,6 +1000,7 @@ async function hasSkillMd(dir) {
965
1000
  }
966
1001
  async function parseSkillMd(skillMdPath) {
967
1002
  try {
1003
+ if (isDeniedPath(skillMdPath)) return null;
968
1004
  const content = await readFile(skillMdPath, "utf-8");
969
1005
  const { data } = matter(content);
970
1006
  if (!data.name || !data.description) {
@@ -984,6 +1020,7 @@ async function parseSkillMd(skillMdPath) {
984
1020
  async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
985
1021
  const skillDirs = [];
986
1022
  if (depth > maxDepth) return skillDirs;
1023
+ if (isDeniedPath(dir)) return skillDirs;
987
1024
  try {
988
1025
  if (await hasSkillMd(dir)) {
989
1026
  skillDirs.push(dir);
@@ -999,9 +1036,51 @@ async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
999
1036
  }
1000
1037
  return skillDirs;
1001
1038
  }
1039
+ async function readMarketplacePluginRoots(basePath) {
1040
+ const filePath = join6(basePath, ".claude-plugin", "marketplace.json");
1041
+ if (!existsSync2(filePath)) return [];
1042
+ try {
1043
+ const raw = await readFile(filePath, "utf-8");
1044
+ const parsed = JSON.parse(raw);
1045
+ const roots = (parsed.plugins ?? []).map((plugin) => typeof plugin.source === "string" ? plugin.source : null).filter(Boolean);
1046
+ return roots.map((root) => normalizeRoot(root)).filter(Boolean);
1047
+ } catch {
1048
+ return [];
1049
+ }
1050
+ }
1051
+ async function collectSkillsFromRoot(root, seenSlugs, skills) {
1052
+ if (!root || !existsSync2(root) || isDeniedPath(root)) return;
1053
+ const skillDirs = await findSkillDirs(root);
1054
+ for (const skillDir of skillDirs) {
1055
+ const skill = await parseSkillMd(join6(skillDir, "SKILL.md"));
1056
+ if (!skill) continue;
1057
+ const slug = basename2(skill.path).toLowerCase();
1058
+ if (seenSlugs.has(slug)) continue;
1059
+ skills.push(skill);
1060
+ seenSlugs.add(slug);
1061
+ }
1062
+ }
1063
+ async function listPluginSkillRoots(basePath) {
1064
+ const pluginsDir = join6(basePath, "plugins");
1065
+ if (!existsSync2(pluginsDir)) return [];
1066
+ try {
1067
+ const entries = await readdir2(pluginsDir, { withFileTypes: true });
1068
+ const roots = [];
1069
+ for (const entry of entries) {
1070
+ if (!entry.isDirectory()) continue;
1071
+ const candidate = join6(pluginsDir, entry.name, "skills");
1072
+ if (existsSync2(candidate)) {
1073
+ roots.push(candidate);
1074
+ }
1075
+ }
1076
+ return roots;
1077
+ } catch {
1078
+ return [];
1079
+ }
1080
+ }
1002
1081
  async function discoverSkills(basePath, subpath) {
1003
1082
  const skills = [];
1004
- const seenNames = /* @__PURE__ */ new Set();
1083
+ const seenSlugs = /* @__PURE__ */ new Set();
1005
1084
  const searchPath = subpath ? join6(basePath, subpath) : basePath;
1006
1085
  if (await hasSkillMd(searchPath)) {
1007
1086
  const skill = await parseSkillMd(join6(searchPath, "SKILL.md"));
@@ -1010,15 +1089,20 @@ async function discoverSkills(basePath, subpath) {
1010
1089
  return skills;
1011
1090
  }
1012
1091
  }
1013
- const prioritySearchDirs = [
1014
- searchPath,
1015
- join6(searchPath, "skills"),
1016
- join6(searchPath, "skills/.curated"),
1017
- join6(searchPath, "skills/.experimental"),
1018
- join6(searchPath, "skills/.system"),
1092
+ const marketplaceRoots = await readMarketplacePluginRoots(searchPath);
1093
+ for (const root of marketplaceRoots) {
1094
+ const skillsRoot = root.toLowerCase().endsWith("/skills") ? root : `${root}/skills`;
1095
+ await collectSkillsFromRoot(join6(searchPath, skillsRoot), seenSlugs, skills);
1096
+ }
1097
+ await collectSkillsFromRoot(join6(searchPath, "skills"), seenSlugs, skills);
1098
+ const pluginRoots = await listPluginSkillRoots(searchPath);
1099
+ for (const root of pluginRoots) {
1100
+ await collectSkillsFromRoot(root, seenSlugs, skills);
1101
+ }
1102
+ await collectSkillsFromRoot(join6(searchPath, ".claude-plugin"), seenSlugs, skills);
1103
+ const agentRoots = [
1019
1104
  join6(searchPath, ".agent/skills"),
1020
1105
  join6(searchPath, ".agents/skills"),
1021
- join6(searchPath, ".claude/skills"),
1022
1106
  join6(searchPath, ".cline/skills"),
1023
1107
  join6(searchPath, ".codex/skills"),
1024
1108
  join6(searchPath, ".commandcode/skills"),
@@ -1030,7 +1114,6 @@ async function discoverSkills(basePath, subpath) {
1030
1114
  join6(searchPath, ".kilocode/skills"),
1031
1115
  join6(searchPath, ".kiro/skills"),
1032
1116
  join6(searchPath, ".neovate/skills"),
1033
- join6(searchPath, ".opencode/skills"),
1034
1117
  join6(searchPath, ".openhands/skills"),
1035
1118
  join6(searchPath, ".pi/skills"),
1036
1119
  join6(searchPath, ".qoder/skills"),
@@ -1039,32 +1122,18 @@ async function discoverSkills(basePath, subpath) {
1039
1122
  join6(searchPath, ".windsurf/skills"),
1040
1123
  join6(searchPath, ".zencoder/skills")
1041
1124
  ];
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
- }
1125
+ for (const root of agentRoots) {
1126
+ await collectSkillsFromRoot(root, seenSlugs, skills);
1059
1127
  }
1060
1128
  if (skills.length === 0) {
1061
1129
  const allSkillDirs = await findSkillDirs(searchPath);
1062
1130
  for (const skillDir of allSkillDirs) {
1063
1131
  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
- }
1132
+ if (!skill) continue;
1133
+ const slug = basename2(skill.path).toLowerCase();
1134
+ if (seenSlugs.has(slug)) continue;
1135
+ skills.push(skill);
1136
+ seenSlugs.add(slug);
1068
1137
  }
1069
1138
  }
1070
1139
  return skills;
@@ -2034,10 +2103,10 @@ function AddScopeScreen() {
2034
2103
  }
2035
2104
 
2036
2105
  // src/tui/screens/AddSkillSelect.tsx
2037
- import { existsSync as existsSync3 } from "fs";
2106
+ import { existsSync as existsSync4 } from "fs";
2038
2107
  import { mkdir as mkdir4, mkdtemp as mkdtemp2, writeFile as writeFile3 } from "fs/promises";
2039
2108
  import { tmpdir as tmpdir3 } from "os";
2040
- import { join as join11 } from "path";
2109
+ import { basename as basename3, join as join11 } from "path";
2041
2110
  import { Box as Box15, Text as Text13 } from "ink";
2042
2111
  import React11 from "react";
2043
2112
 
@@ -2351,7 +2420,7 @@ async function resolveRemoteSkill(url) {
2351
2420
  }
2352
2421
 
2353
2422
  // src/marketplace.ts
2354
- import { existsSync as existsSync2, statSync } from "fs";
2423
+ import { existsSync as existsSync3, statSync } from "fs";
2355
2424
  import { readFile as readFile3 } from "fs/promises";
2356
2425
  import { dirname as dirname3, join as join10, posix } from "path";
2357
2426
  function toRecord(value) {
@@ -2418,14 +2487,14 @@ function isMarketplaceInput(input) {
2418
2487
  return input.toLowerCase().endsWith("marketplace.json");
2419
2488
  }
2420
2489
  function resolveLocalMarketplacePath(input) {
2421
- if (!existsSync2(input)) return null;
2490
+ if (!existsSync3(input)) return null;
2422
2491
  const stats = statSync(input);
2423
2492
  if (stats.isFile() && input.toLowerCase().endsWith("marketplace.json")) {
2424
2493
  return input;
2425
2494
  }
2426
2495
  if (stats.isDirectory()) {
2427
2496
  const candidate = join10(input, ".claude-plugin", "marketplace.json");
2428
- if (existsSync2(candidate)) return candidate;
2497
+ if (existsSync3(candidate)) return candidate;
2429
2498
  }
2430
2499
  return null;
2431
2500
  }
@@ -2799,7 +2868,7 @@ function AddSkillSelectScreen() {
2799
2868
  throw new Error("Local path is missing.");
2800
2869
  }
2801
2870
  skillsDir = parsed.localPath;
2802
- if (!existsSync3(skillsDir)) {
2871
+ if (!existsSync4(skillsDir)) {
2803
2872
  throw new Error(`Local path does not exist: ${skillsDir}`);
2804
2873
  }
2805
2874
  } else {
@@ -2843,6 +2912,9 @@ function AddSkillSelectScreen() {
2843
2912
  if (autoSelection.status === "error") {
2844
2913
  throw new Error(autoSelection.message);
2845
2914
  }
2915
+ if (autoSelection.status === "prompt" && autoSelection.message) {
2916
+ setFlash(autoSelection.message);
2917
+ }
2846
2918
  if (tempDir) {
2847
2919
  keepTempDir = true;
2848
2920
  }
@@ -2864,7 +2936,7 @@ function AddSkillSelectScreen() {
2864
2936
  return () => {
2865
2937
  cancelled = true;
2866
2938
  };
2867
- }, [source, addSkill.skills, updateAddSkill, navigateTo, options]);
2939
+ }, [source, addSkill.skills, updateAddSkill, navigateTo, options, setFlash]);
2868
2940
  React11.useEffect(() => {
2869
2941
  if (invocation.source) {
2870
2942
  setBackHandler(() => {
@@ -2963,16 +3035,18 @@ function AddSkillSelectScreen() {
2963
3035
  )
2964
3036
  ] });
2965
3037
  }
3038
+ function matchesSkillName(skill, input) {
3039
+ const normalized = input.toLowerCase();
3040
+ const byName = skill.name.toLowerCase() === normalized;
3041
+ const byPath = basename3(skill.path).toLowerCase() === normalized;
3042
+ return byName || byPath;
3043
+ }
2966
3044
  function autoSelect(skills, options) {
2967
3045
  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
- );
3046
+ const selected = skills.filter((s) => options.skill?.some((name) => matchesSkillName(s, name)));
2973
3047
  if (selected.length === 0) {
2974
3048
  return {
2975
- status: "error",
3049
+ status: "prompt",
2976
3050
  message: `No matching skills found for: ${options.skill.join(", ")}`
2977
3051
  };
2978
3052
  }
@@ -3140,7 +3214,7 @@ import React14 from "react";
3140
3214
 
3141
3215
  // src/installed-skills.ts
3142
3216
  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";
3217
+ import { basename as basename4, join as join12 } from "path";
3144
3218
  import matter6 from "gray-matter";
3145
3219
  function getAgentSkillsDir(agent, scope, cwd) {
3146
3220
  const config = agents[agent];
@@ -3215,7 +3289,7 @@ async function listSkillsForAgent(agent, scope, cwd = process.cwd()) {
3215
3289
  }
3216
3290
  async function findSkillInstallations(skillName, scope, cwd = process.cwd()) {
3217
3291
  const installs = [];
3218
- const sanitized = basename3(getCanonicalPath(skillName, { global: scope === "global", cwd }));
3292
+ const sanitized = basename4(getCanonicalPath(skillName, { global: scope === "global", cwd }));
3219
3293
  if (!sanitized) {
3220
3294
  return installs;
3221
3295
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playbooks",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Install agent skills, MCPs and docs into your coding agents from any git repository.",
5
5
  "type": "module",
6
6
  "bin": {