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.
- package/dist/index.js +121 -47
- 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.
|
|
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
|
|
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
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
join6(searchPath,
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
|
1043
|
-
|
|
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 (
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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: "
|
|
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
|
|
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 =
|
|
3292
|
+
const sanitized = basename4(getCanonicalPath(skillName, { global: scope === "global", cwd }));
|
|
3219
3293
|
if (!sanitized) {
|
|
3220
3294
|
return installs;
|
|
3221
3295
|
}
|