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.
- package/dist/index.js +116 -48
- 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.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
|
|
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
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
join6(searchPath,
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
|
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
|
-
}
|
|
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 (
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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: "
|
|
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
|
|
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 =
|
|
3286
|
+
const sanitized = basename4(getCanonicalPath(skillName, { global: scope === "global", cwd }));
|
|
3219
3287
|
if (!sanitized) {
|
|
3220
3288
|
return installs;
|
|
3221
3289
|
}
|