master-skill 0.10.0 → 0.10.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/ETHICS.md +23 -17
- package/README.md +18 -11
- package/README_EN.md +22 -13
- package/SKILL.md +5 -5
- package/bin/cli.mjs +309 -77
- package/gemini-extension.json +1 -1
- package/hooks/run-hook.cmd +18 -5
- package/hooks/tests/test_run_hook.sh +114 -0
- package/hooks/tests/test_run_hook_cmd.sh +94 -0
- package/masters/.gitkeep +0 -0
- package/package.json +8 -2
- package/prebuilt/compare/SKILL.md +5 -5
- package/prebuilt/master-ajahn-chah/SKILL.md +13 -11
- package/prebuilt/master-ajahn-chah/meta.json +8 -0
- package/prebuilt/master-ajahn-chah/references/voice.md +1 -1
- package/prebuilt/master-atisha/SKILL.md +13 -11
- package/prebuilt/master-atisha/meta.json +8 -0
- package/prebuilt/master-atisha/references/voice.md +1 -1
- package/prebuilt/master-buddhaghosa/SKILL.md +13 -11
- package/prebuilt/master-buddhaghosa/meta.json +8 -0
- package/prebuilt/master-buddhaghosa/references/voice.md +1 -1
- package/prebuilt/master-fazang/SKILL.md +3 -3
- package/prebuilt/master-fazang/meta.json +8 -0
- package/prebuilt/master-huineng/SKILL.md +3 -3
- package/prebuilt/master-huineng/meta.json +8 -0
- package/prebuilt/master-kumarajiva/SKILL.md +3 -3
- package/prebuilt/master-kumarajiva/meta.json +8 -0
- package/prebuilt/master-mahasi-sayadaw/SKILL.md +13 -11
- package/prebuilt/master-mahasi-sayadaw/meta.json +8 -0
- package/prebuilt/master-mahasi-sayadaw/references/voice.md +2 -2
- package/prebuilt/master-milarepa/SKILL.md +13 -11
- package/prebuilt/master-milarepa/meta.json +8 -0
- package/prebuilt/master-milarepa/references/voice.md +1 -1
- package/prebuilt/master-nagarjuna/SKILL.md +3 -3
- package/prebuilt/master-nagarjuna/meta.json +8 -0
- package/prebuilt/master-ouyi/SKILL.md +3 -3
- package/prebuilt/master-ouyi/meta.json +8 -0
- package/prebuilt/master-tsongkhapa/SKILL.md +13 -11
- package/prebuilt/master-tsongkhapa/meta.json +8 -0
- package/prebuilt/master-tsongkhapa/references/voice.md +1 -1
- package/prebuilt/master-xuanzang/SKILL.md +3 -3
- package/prebuilt/master-xuanzang/meta.json +8 -0
- package/prebuilt/master-xuyun/SKILL.md +3 -3
- package/prebuilt/master-xuyun/meta.json +8 -0
- package/prebuilt/master-yinguang/SKILL.md +3 -3
- package/prebuilt/master-yinguang/meta.json +8 -0
- package/prebuilt/master-zhiyi/SKILL.md +3 -3
- package/prebuilt/master-zhiyi/meta.json +8 -0
- package/prompts/correction_handler.md +104 -0
- package/prompts/doctrine_reviewer.md +61 -0
- package/prompts/intake.md +62 -0
- package/prompts/merger.md +62 -0
- package/prompts/rag_instructions.md +54 -0
- package/prompts/sutra_analyzer.md +83 -0
- package/prompts/teaching_builder.md +41 -0
- package/prompts/voice_analyzer.md +92 -0
- package/prompts/voice_builder.md +48 -0
- package/prompts/voice_reviewer.md +66 -0
- package/references/README.md +12 -0
- package/references/ethics-runtime.md +112 -0
- package/references/fojin-api.md +223 -0
- package/references/source-conventions.md +129 -0
- package/references/teaching-modes.md +84 -0
- package/references/traditions.md +72 -0
- package/references/workflow-details.md +361 -0
- package/requirements.txt +6 -0
- package/scripts/select-fidelity-smoke.py +78 -0
- package/scripts/test-fidelity.py +19 -2
- package/scripts/tests/test_select_fidelity_smoke.py +142 -0
- package/scripts/tests/test_validate_citation_contract.py +408 -0
- package/scripts/tests/test_validate_workflow.py +265 -0
- package/scripts/validate-citation-contract.py +193 -0
- package/scripts/verify_citations.py +8 -1
- package/skill-catalog.json +147 -0
- package/tools/cross_reference.py +365 -0
- package/tools/fojin_bridge.py +146 -0
- package/tools/master_builder.py +341 -0
- package/tools/rag_query.py +336 -0
- package/tools/skill_writer.py +230 -0
- package/tools/sutra_collector.py +237 -0
- package/tools/verify_sources.py +512 -0
- package/tools/version_manager.py +88 -0
package/bin/cli.mjs
CHANGED
|
@@ -8,8 +8,11 @@ import { fileURLToPath } from "url";
|
|
|
8
8
|
// fileURLToPath (not new URL().pathname) — on Windows the URL pathname is
|
|
9
9
|
// "/C:/…", which fs cannot resolve, so every command saw an empty prebuilt/.
|
|
10
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const
|
|
11
|
+
const PACKAGE_ROOT = path.join(__dirname, "..");
|
|
12
|
+
const PREBUILT = path.join(PACKAGE_ROOT, "prebuilt");
|
|
13
|
+
const CATALOG_PATH = path.join(PACKAGE_ROOT, "skill-catalog.json");
|
|
12
14
|
const SKILLS_DIR = path.join(os.homedir(), ".claude", "skills");
|
|
15
|
+
const SKILL_KINDS = new Set(["persona", "teaching-mode", "generator"]);
|
|
13
16
|
|
|
14
17
|
// --- helpers ---
|
|
15
18
|
|
|
@@ -27,7 +30,119 @@ function pkgVersion() {
|
|
|
27
30
|
// Names feed into path.join + rmSync; reject separators / ".." so a typo
|
|
28
31
|
// like "../foo" can never escape PREBUILT or SKILLS_DIR.
|
|
29
32
|
function isSafeName(name) {
|
|
30
|
-
return /^[A-Za-z0-9_-]+$/.test(name);
|
|
33
|
+
return typeof name === "string" && /^[A-Za-z0-9_-]+$/.test(name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isSafeRelativePath(value, { allowDot = false } = {}) {
|
|
37
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
38
|
+
if (allowDot && value === ".") return true;
|
|
39
|
+
if (value === "." || value.includes("\\") || path.posix.isAbsolute(value)) return false;
|
|
40
|
+
if (/^[A-Za-z]:/.test(value)) return false;
|
|
41
|
+
const normalized = path.posix.normalize(value);
|
|
42
|
+
return normalized === value && normalized !== ".." && !normalized.startsWith("../");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function invalidCatalog(message) {
|
|
46
|
+
throw new Error(`Invalid skill catalog: ${message}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadCatalog() {
|
|
50
|
+
let catalog;
|
|
51
|
+
try {
|
|
52
|
+
catalog = JSON.parse(fs.readFileSync(CATALOG_PATH, "utf8"));
|
|
53
|
+
} catch (err) {
|
|
54
|
+
invalidCatalog(`cannot read ${path.basename(CATALOG_PATH)} (${err.message})`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (catalog?.version !== 1) invalidCatalog("version must be 1");
|
|
58
|
+
if (!Array.isArray(catalog.skills) || catalog.skills.length === 0) {
|
|
59
|
+
invalidCatalog("skills must be a non-empty array");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const uniqueFields = {
|
|
63
|
+
name: new Set(),
|
|
64
|
+
source: new Set(),
|
|
65
|
+
install_dir: new Set(),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
for (const [index, skill] of catalog.skills.entries()) {
|
|
69
|
+
if (!skill || typeof skill !== "object" || Array.isArray(skill)) {
|
|
70
|
+
invalidCatalog(`skills[${index}] must be an object`);
|
|
71
|
+
}
|
|
72
|
+
if (!isSafeName(skill.name)) {
|
|
73
|
+
invalidCatalog(`skills[${index}].name must be a safe non-empty string`);
|
|
74
|
+
}
|
|
75
|
+
if (!SKILL_KINDS.has(skill.kind)) {
|
|
76
|
+
invalidCatalog(`skills[${index}].kind must be persona, teaching-mode, or generator`);
|
|
77
|
+
}
|
|
78
|
+
if (!isSafeRelativePath(skill.source, { allowDot: true })) {
|
|
79
|
+
invalidCatalog(`skills[${index}].source must be a safe relative path`);
|
|
80
|
+
}
|
|
81
|
+
if (!isSafeName(skill.install_dir)) {
|
|
82
|
+
invalidCatalog(`skills[${index}].install_dir must be a safe non-empty string`);
|
|
83
|
+
}
|
|
84
|
+
if (!Array.isArray(skill.aliases)) {
|
|
85
|
+
invalidCatalog(`skills[${index}].aliases must be an array`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const field of Object.keys(uniqueFields)) {
|
|
89
|
+
if (uniqueFields[field].has(skill[field])) {
|
|
90
|
+
invalidCatalog(`duplicate ${field} "${skill[field]}"`);
|
|
91
|
+
}
|
|
92
|
+
uniqueFields[field].add(skill[field]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sourcePath = path.resolve(PACKAGE_ROOT, skill.source);
|
|
96
|
+
if (!fs.existsSync(sourcePath)) invalidCatalog(`source does not exist: ${skill.source}`);
|
|
97
|
+
if (!fs.statSync(sourcePath).isDirectory()) {
|
|
98
|
+
invalidCatalog(`source is not a directory: ${skill.source}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (skill.kind === "generator") {
|
|
102
|
+
if (!Array.isArray(skill.bundle_paths) || skill.bundle_paths.length === 0) {
|
|
103
|
+
invalidCatalog(`generator "${skill.name}" must declare bundle_paths`);
|
|
104
|
+
}
|
|
105
|
+
const bundlePaths = new Set();
|
|
106
|
+
for (const bundlePath of skill.bundle_paths) {
|
|
107
|
+
if (!isSafeRelativePath(bundlePath)) {
|
|
108
|
+
invalidCatalog(`generator "${skill.name}" has an unsafe bundle path`);
|
|
109
|
+
}
|
|
110
|
+
if (bundlePaths.has(bundlePath)) {
|
|
111
|
+
invalidCatalog(`generator "${skill.name}" has duplicate bundle path "${bundlePath}"`);
|
|
112
|
+
}
|
|
113
|
+
bundlePaths.add(bundlePath);
|
|
114
|
+
if (!fs.existsSync(path.resolve(sourcePath, bundlePath))) {
|
|
115
|
+
invalidCatalog(`bundle path does not exist: ${bundlePath}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} else if (skill.bundle_paths !== undefined) {
|
|
119
|
+
invalidCatalog(`only generator skills may declare bundle_paths`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Register every canonical name before considering aliases so token
|
|
124
|
+
// ownership is independent of catalog record order. An alias may repeat its
|
|
125
|
+
// own canonical name, but it may never shadow another skill's canonical name.
|
|
126
|
+
const aliases = new Set();
|
|
127
|
+
const tokenOwners = new Map(
|
|
128
|
+
catalog.skills.map((skill) => [skill.name, skill.name])
|
|
129
|
+
);
|
|
130
|
+
for (const [index, skill] of catalog.skills.entries()) {
|
|
131
|
+
for (const alias of skill.aliases) {
|
|
132
|
+
if (!isSafeName(alias)) {
|
|
133
|
+
invalidCatalog(`skills[${index}].aliases contains an unsafe value`);
|
|
134
|
+
}
|
|
135
|
+
if (aliases.has(alias)) invalidCatalog(`duplicate alias "${alias}"`);
|
|
136
|
+
aliases.add(alias);
|
|
137
|
+
const existingOwner = tokenOwners.get(alias);
|
|
138
|
+
if (existingOwner && existingOwner !== skill.name) {
|
|
139
|
+
invalidCatalog(`alias "${alias}" conflicts with skill "${existingOwner}"`);
|
|
140
|
+
}
|
|
141
|
+
tokenOwners.set(alias, skill.name);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return catalog;
|
|
31
146
|
}
|
|
32
147
|
|
|
33
148
|
function cpR(src, dest) {
|
|
@@ -40,6 +155,65 @@ function cpR(src, dest) {
|
|
|
40
155
|
}
|
|
41
156
|
}
|
|
42
157
|
|
|
158
|
+
function copyGeneratorBundle(skill, src, dest) {
|
|
159
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
160
|
+
for (const bundlePath of skill.bundle_paths) {
|
|
161
|
+
const bundleSource = path.join(src, bundlePath);
|
|
162
|
+
const bundleDest = path.join(dest, bundlePath);
|
|
163
|
+
if (fs.statSync(bundleSource).isDirectory()) {
|
|
164
|
+
cpR(bundleSource, bundleDest);
|
|
165
|
+
} else {
|
|
166
|
+
fs.mkdirSync(path.dirname(bundleDest), { recursive: true });
|
|
167
|
+
fs.copyFileSync(bundleSource, bundleDest);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function replaceGeneratorInstall(skill, src, dest) {
|
|
173
|
+
const staging = fs.mkdtempSync(
|
|
174
|
+
path.join(SKILLS_DIR, `.${skill.install_dir}-staging-`)
|
|
175
|
+
);
|
|
176
|
+
let backup = null;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
copyGeneratorBundle(skill, src, staging);
|
|
180
|
+
|
|
181
|
+
// Generated personas are user data, not package runtime. Merge them into
|
|
182
|
+
// the staged bundle before replacing the old install so reinstall/update
|
|
183
|
+
// can still remove stale runtime files without erasing masters/*.
|
|
184
|
+
const existingMasters = path.join(dest, "masters");
|
|
185
|
+
if (fs.existsSync(existingMasters)) {
|
|
186
|
+
cpR(existingMasters, path.join(staging, "masters"));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (fs.existsSync(dest)) {
|
|
190
|
+
backup = fs.mkdtempSync(
|
|
191
|
+
path.join(SKILLS_DIR, `.${skill.install_dir}-backup-`)
|
|
192
|
+
);
|
|
193
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
194
|
+
fs.renameSync(dest, backup);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
fs.renameSync(staging, dest);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if (backup && fs.existsSync(backup) && !fs.existsSync(dest)) {
|
|
201
|
+
fs.renameSync(backup, dest);
|
|
202
|
+
backup = null;
|
|
203
|
+
}
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (backup) {
|
|
208
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
209
|
+
backup = null;
|
|
210
|
+
}
|
|
211
|
+
} finally {
|
|
212
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
213
|
+
if (backup) fs.rmSync(backup, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
43
217
|
function parseFrontmatter(filepath) {
|
|
44
218
|
// \r?\n: a CRLF checkout (git autocrlf on Windows) must not blank out
|
|
45
219
|
// every description.
|
|
@@ -97,12 +271,47 @@ function printJson(payload) {
|
|
|
97
271
|
console.log(JSON.stringify(payload, null, 2));
|
|
98
272
|
}
|
|
99
273
|
|
|
274
|
+
let CATALOG;
|
|
275
|
+
try {
|
|
276
|
+
CATALOG = loadCatalog();
|
|
277
|
+
} catch (err) {
|
|
278
|
+
console.error(err.message);
|
|
279
|
+
process.exitCode = 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function catalogSkills() {
|
|
283
|
+
return CATALOG.skills.map((skill) => {
|
|
284
|
+
const skillMd = path.join(PACKAGE_ROOT, skill.source, "SKILL.md");
|
|
285
|
+
const fm = fs.existsSync(skillMd) ? parseFrontmatter(skillMd) : {};
|
|
286
|
+
return { ...skill, description: fm.description || "" };
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function resolveSkill(input) {
|
|
291
|
+
return catalogSkills().find(
|
|
292
|
+
(skill) => skill.name === input || skill.aliases.includes(input)
|
|
293
|
+
) || null;
|
|
294
|
+
}
|
|
295
|
+
|
|
100
296
|
// --- commands ---
|
|
101
297
|
|
|
102
298
|
function listData() {
|
|
103
299
|
const masters = availableMasters();
|
|
300
|
+
const skills = catalogSkills();
|
|
301
|
+
const categoryCounts = skills.reduce((counts, skill) => {
|
|
302
|
+
counts[skill.kind] += 1;
|
|
303
|
+
return counts;
|
|
304
|
+
}, { persona: 0, "teaching-mode": 0, generator: 0 });
|
|
104
305
|
return {
|
|
105
306
|
count: masters.length,
|
|
307
|
+
skillCount: skills.length,
|
|
308
|
+
categoryCounts,
|
|
309
|
+
skills: skills.map(({ name, kind, install_dir, description }) => ({
|
|
310
|
+
name,
|
|
311
|
+
kind,
|
|
312
|
+
installDir: install_dir,
|
|
313
|
+
description,
|
|
314
|
+
})),
|
|
106
315
|
masters: masters.map((m) => ({
|
|
107
316
|
name: m.name,
|
|
108
317
|
slug: m.name.replace(/^master-/, ""),
|
|
@@ -118,16 +327,27 @@ function cmdList({ json = false } = {}) {
|
|
|
118
327
|
return;
|
|
119
328
|
}
|
|
120
329
|
|
|
121
|
-
const
|
|
122
|
-
if (!
|
|
123
|
-
console.log("No
|
|
330
|
+
const skills = data.skills;
|
|
331
|
+
if (!skills.length) {
|
|
332
|
+
console.log("No installable skills found.");
|
|
124
333
|
return;
|
|
125
334
|
}
|
|
126
|
-
console.log(`\nAvailable masters (${data.count})
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
335
|
+
console.log(`\nAvailable masters (${data.count}); installable skills (${data.skillCount}):`);
|
|
336
|
+
const groups = [
|
|
337
|
+
["persona", "Personas"],
|
|
338
|
+
["teaching-mode", "Teaching modes"],
|
|
339
|
+
["generator", "Generator"],
|
|
340
|
+
];
|
|
341
|
+
const nameW = Math.max(...skills.map((skill) => skill.name.length), 4);
|
|
342
|
+
for (const [kind, label] of groups) {
|
|
343
|
+
const group = skills.filter((skill) => skill.kind === kind);
|
|
344
|
+
console.log(`\n${label} (${group.length}):`);
|
|
345
|
+
for (const skill of group) {
|
|
346
|
+
const desc = skill.description.length > 80
|
|
347
|
+
? skill.description.slice(0, 77) + "..."
|
|
348
|
+
: skill.description;
|
|
349
|
+
console.log(` ${skill.name.padEnd(nameW)} ${desc}`);
|
|
350
|
+
}
|
|
131
351
|
}
|
|
132
352
|
console.log();
|
|
133
353
|
}
|
|
@@ -153,30 +373,34 @@ function cmdInstall(names) {
|
|
|
153
373
|
failed++;
|
|
154
374
|
continue;
|
|
155
375
|
}
|
|
156
|
-
const
|
|
157
|
-
if (!
|
|
158
|
-
console.log(` ✗ ${name} — not found in
|
|
376
|
+
const skill = resolveSkill(name);
|
|
377
|
+
if (!skill) {
|
|
378
|
+
console.log(` ✗ ${name} — not found in skill catalog`);
|
|
159
379
|
failed++;
|
|
160
380
|
continue;
|
|
161
381
|
}
|
|
162
|
-
const
|
|
163
|
-
const dest = path.join(SKILLS_DIR,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
382
|
+
const src = path.join(PACKAGE_ROOT, skill.source);
|
|
383
|
+
const dest = path.join(SKILLS_DIR, skill.install_dir);
|
|
384
|
+
if (skill.kind === "generator") {
|
|
385
|
+
replaceGeneratorInstall(skill, src, dest);
|
|
386
|
+
} else {
|
|
387
|
+
// Clear any previous install first: files renamed or removed upstream
|
|
388
|
+
// must not linger as stale skill content under ~/.claude/skills/.
|
|
389
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
390
|
+
cpR(src, dest);
|
|
391
|
+
}
|
|
168
392
|
console.log(` ✓ ${name} → ${dest}`);
|
|
169
393
|
}
|
|
170
394
|
return failed;
|
|
171
395
|
}
|
|
172
396
|
|
|
173
397
|
function cmdInstallAll(label = "Installing") {
|
|
174
|
-
const all =
|
|
398
|
+
const all = catalogSkills().map((skill) => skill.name);
|
|
175
399
|
if (!all.length) {
|
|
176
|
-
console.log("No
|
|
400
|
+
console.log("No skills available.");
|
|
177
401
|
return 1;
|
|
178
402
|
}
|
|
179
|
-
console.log(`${label} all ${all.length}
|
|
403
|
+
console.log(`${label} all ${all.length} skills...\n`);
|
|
180
404
|
return cmdInstall(all);
|
|
181
405
|
}
|
|
182
406
|
|
|
@@ -188,12 +412,16 @@ function cmdUninstall(names) {
|
|
|
188
412
|
failed++;
|
|
189
413
|
continue;
|
|
190
414
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
415
|
+
const skill = resolveSkill(name);
|
|
416
|
+
if (!skill) {
|
|
417
|
+
console.log(` ✗ ${name} — not found in skill catalog`);
|
|
418
|
+
failed++;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
// Keep removing legacy bare persona directories when they exist, while
|
|
422
|
+
// resolving every public name through the catalog's canonical target.
|
|
423
|
+
const candidates = [path.join(SKILLS_DIR, skill.install_dir)];
|
|
424
|
+
if (name !== skill.install_dir) candidates.push(path.join(SKILLS_DIR, name));
|
|
197
425
|
const dest = candidates.find((p) => fs.existsSync(p));
|
|
198
426
|
if (!dest) {
|
|
199
427
|
console.log(` ✗ ${name} — not installed`);
|
|
@@ -341,25 +569,27 @@ function showHelp() {
|
|
|
341
569
|
master-skill v${pkgVersion()} — Buddhist Master AI Skills installer
|
|
342
570
|
|
|
343
571
|
Usage:
|
|
344
|
-
master-skill install <name...> Install
|
|
345
|
-
master-skill install --all Install all available
|
|
346
|
-
master-skill update --all Reinstall all
|
|
347
|
-
master-skill list List available
|
|
348
|
-
master-skill list --json Print available
|
|
572
|
+
master-skill install <name...> Install skills to ~/.claude/skills/
|
|
573
|
+
master-skill install --all Install all 19 available skills
|
|
574
|
+
master-skill update --all Reinstall all skills, clearing stale files
|
|
575
|
+
master-skill list List available skills
|
|
576
|
+
master-skill list --json Print available skills as JSON
|
|
349
577
|
master-skill inspect <name> Show source/runtime metadata for one master
|
|
350
578
|
master-skill inspect <name> --json
|
|
351
579
|
master-skill doctor Check local install and runtime paths
|
|
352
580
|
master-skill doctor --json Print diagnostics as JSON
|
|
353
|
-
master-skill uninstall <name...> Remove installed
|
|
581
|
+
master-skill uninstall <name...> Remove installed skills
|
|
354
582
|
master-skill --version Print version
|
|
355
583
|
master-skill --help Show this help
|
|
356
584
|
|
|
357
|
-
|
|
358
|
-
|
|
585
|
+
Persona names accept both short (zhiyi) and full (master-zhiyi) forms.
|
|
586
|
+
Teaching modes and create-master use their public catalog names.
|
|
359
587
|
|
|
360
588
|
Examples:
|
|
361
589
|
npx master-skill install zhiyi fazang
|
|
362
590
|
npx master-skill install master-milarepa master-tsongkhapa
|
|
591
|
+
npx master-skill install compare-masters
|
|
592
|
+
npx master-skill install create-master
|
|
363
593
|
npx master-skill install --all
|
|
364
594
|
npx master-skill update --all
|
|
365
595
|
npx master-skill list
|
|
@@ -371,48 +601,50 @@ Examples:
|
|
|
371
601
|
|
|
372
602
|
// --- main ---
|
|
373
603
|
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (
|
|
604
|
+
if (CATALOG) {
|
|
605
|
+
const args = process.argv.slice(2);
|
|
606
|
+
const json = args.includes("--json");
|
|
607
|
+
const positionalArgs = args.filter((arg) => arg !== "--json");
|
|
608
|
+
const cmd = positionalArgs[0];
|
|
609
|
+
|
|
610
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
611
|
+
showHelp();
|
|
612
|
+
} else if (cmd === "--version" || cmd === "-v") {
|
|
613
|
+
console.log(pkgVersion());
|
|
614
|
+
} else if (cmd === "list") {
|
|
615
|
+
cmdList({ json });
|
|
616
|
+
} else if (cmd === "doctor") {
|
|
617
|
+
if (cmdDoctor({ json }) > 0) process.exitCode = 1;
|
|
618
|
+
} else if (cmd === "inspect") {
|
|
619
|
+
if (cmdInspect(positionalArgs[1], { json }) > 0) process.exitCode = 1;
|
|
620
|
+
} else if (cmd === "install") {
|
|
621
|
+
const rest = positionalArgs.slice(1);
|
|
622
|
+
if (rest.includes("--all")) {
|
|
623
|
+
if (cmdInstallAll("Installing") > 0) process.exitCode = 1;
|
|
624
|
+
} else if (rest.length === 0) {
|
|
625
|
+
console.log("Usage: master-skill install <name...> | --all");
|
|
626
|
+
process.exitCode = 1;
|
|
627
|
+
} else {
|
|
628
|
+
if (cmdInstall(rest) > 0) process.exitCode = 1;
|
|
629
|
+
}
|
|
630
|
+
} else if (cmd === "update") {
|
|
631
|
+
const rest = positionalArgs.slice(1);
|
|
632
|
+
if (rest.length === 1 && rest[0] === "--all") {
|
|
633
|
+
if (cmdInstallAll("Updating") > 0) process.exitCode = 1;
|
|
634
|
+
} else {
|
|
635
|
+
console.log("Usage: master-skill update --all");
|
|
636
|
+
process.exitCode = 1;
|
|
637
|
+
}
|
|
638
|
+
} else if (cmd === "uninstall") {
|
|
639
|
+
const rest = positionalArgs.slice(1);
|
|
640
|
+
if (rest.length === 0) {
|
|
641
|
+
console.log("Usage: master-skill uninstall <name...>");
|
|
642
|
+
process.exitCode = 1;
|
|
643
|
+
} else {
|
|
644
|
+
if (cmdUninstall(rest) > 0) process.exitCode = 1;
|
|
645
|
+
}
|
|
403
646
|
} else {
|
|
404
|
-
console.log(
|
|
647
|
+
console.log(`Unknown command: ${cmd}\nRun master-skill --help for usage.`);
|
|
405
648
|
process.exitCode = 1;
|
|
406
649
|
}
|
|
407
|
-
} else if (cmd === "uninstall") {
|
|
408
|
-
const rest = positionalArgs.slice(1);
|
|
409
|
-
if (rest.length === 0) {
|
|
410
|
-
console.log("Usage: master-skill uninstall <name...>");
|
|
411
|
-
process.exitCode = 1;
|
|
412
|
-
} else {
|
|
413
|
-
if (cmdUninstall(rest) > 0) process.exitCode = 1;
|
|
414
|
-
}
|
|
415
|
-
} else {
|
|
416
|
-
console.log(`Unknown command: ${cmd}\nRun master-skill --help for usage.`);
|
|
417
|
-
process.exitCode = 1;
|
|
418
650
|
}
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "master-skill",
|
|
3
3
|
"description": "FoJin-powered Buddhist AI persona framework — source-grounded, boundary-aware, fidelity-tested, runtime-ready. 15 prebuilt masters across 印度/汉传/藏传/南传.",
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.1",
|
|
5
5
|
"contextFileName": "GEMINI.md"
|
|
6
6
|
}
|
package/hooks/run-hook.cmd
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
: ; # Polyglot wrapper — runs as cmd.exe on Windows, bash on Unix
|
|
2
|
-
: ; exec bash "$0" "$@"
|
|
3
|
-
: ;
|
|
1
|
+
: ; # Polyglot wrapper — runs as cmd.exe on Windows, bash on Unix.
|
|
2
|
+
: ; # Dispatch $1 by name. This line once read `exec bash "$0" "$@"`,
|
|
3
|
+
: ; # which re-exec'd this same file forever; hooks.json runs the wrapper
|
|
4
|
+
: ; # with "async": false, so every session start spun until the harness
|
|
5
|
+
: ; # hook timeout while 2>/dev/null hid the cause. Keep this half POSIX —
|
|
6
|
+
: ; # the file has no shebang, so /bin/sh interprets it, and that is dash
|
|
7
|
+
: ; # on Debian/Ubuntu where ${@:2} is a "Bad substitution" error.
|
|
8
|
+
: ; HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
: ; HOOK="${1:-}"
|
|
10
|
+
: ; [ "$#" -gt 0 ] && shift
|
|
11
|
+
: ; exec bash "$HOOK_DIR/$HOOK" "$@"
|
|
12
|
+
: ; exit 1
|
|
4
13
|
@echo off
|
|
5
14
|
setlocal enabledelayedexpansion
|
|
6
15
|
|
|
@@ -15,14 +24,18 @@ for %%B in (
|
|
|
15
24
|
) do (
|
|
16
25
|
if exist %%B (
|
|
17
26
|
%%B "%HOOK_DIR%%HOOK%" %2 %3 %4 %5
|
|
18
|
-
|
|
27
|
+
rem !...!, not %...%: cmd.exe expands %ERRORLEVEL% while parsing the
|
|
28
|
+
rem whole parenthesized block, so it would freeze to the value from
|
|
29
|
+
rem before the loop and report every hook failure as success. This is
|
|
30
|
+
rem what `setlocal enabledelayedexpansion` above is for.
|
|
31
|
+
exit /b !ERRORLEVEL!
|
|
19
32
|
)
|
|
20
33
|
)
|
|
21
34
|
|
|
22
35
|
:: Fallback: try bash from PATH
|
|
23
36
|
where bash >nul 2>&1 && (
|
|
24
37
|
bash "%HOOK_DIR%%HOOK%" %2 %3 %4 %5
|
|
25
|
-
exit /b
|
|
38
|
+
exit /b !ERRORLEVEL!
|
|
26
39
|
)
|
|
27
40
|
|
|
28
41
|
:: No bash found — exit silently
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tests for hooks/run-hook.cmd — the polyglot cmd.exe/Unix wrapper that
|
|
3
|
+
# hooks.json invokes for every SessionStart.
|
|
4
|
+
#
|
|
5
|
+
# The wrapper has no shebang: on Unix, execve returns ENOEXEC and the
|
|
6
|
+
# CALLING shell interprets it. That shell is /bin/sh (dash on Debian and
|
|
7
|
+
# Ubuntu), not necessarily bash — so every case below is driven through
|
|
8
|
+
# both `bash -c` and `sh -c`, mirroring how a real hook invocation lands.
|
|
9
|
+
#
|
|
10
|
+
# Regressions these cases pin down:
|
|
11
|
+
#
|
|
12
|
+
# 1. `exec bash "$0" "$@"` re-execs the wrapper itself, spinning forever.
|
|
13
|
+
# hooks.json runs this with "async": false, so every startup/clear/
|
|
14
|
+
# compact blocked until the harness hook timeout, with 2>/dev/null
|
|
15
|
+
# swallowing any sign of it.
|
|
16
|
+
# 2. `${@:2}` is a bashism. Under dash it is a "Bad substitution" error,
|
|
17
|
+
# trading the hang for a silent failure on half the installed base.
|
|
18
|
+
# 3. The wrapper must actually dispatch $1; an unknown hook name has to
|
|
19
|
+
# fail loudly rather than hang or report success.
|
|
20
|
+
#
|
|
21
|
+
# Exit non-zero on any failed assertion.
|
|
22
|
+
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
26
|
+
WRAPPER="$SCRIPT_DIR/../run-hook.cmd"
|
|
27
|
+
TIMEOUT_SECS=10
|
|
28
|
+
|
|
29
|
+
if [ ! -f "$WRAPPER" ]; then
|
|
30
|
+
echo "FAIL: cannot find $WRAPPER" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
PASS=0
|
|
35
|
+
FAIL=0
|
|
36
|
+
|
|
37
|
+
# Run the wrapper under $1 (bash|sh) and report "<exit>|<stdout>".
|
|
38
|
+
# A timeout surfaces as exit 124, which is what the self-exec loop produces.
|
|
39
|
+
run_wrapper() {
|
|
40
|
+
local shell="$1"
|
|
41
|
+
shift
|
|
42
|
+
local out
|
|
43
|
+
local rc
|
|
44
|
+
out=$(timeout "$TIMEOUT_SECS" "$shell" -c "'$WRAPPER' $*" 2>/dev/null)
|
|
45
|
+
rc=$?
|
|
46
|
+
printf '%s|%s' "$rc" "$out"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# --- Cases 1-2: the hook runs to completion and emits valid JSON ---------
|
|
50
|
+
# Driven under both shells: bash alone would not catch the ${@:2} bashism,
|
|
51
|
+
# and sh alone would not catch a bash-only regression.
|
|
52
|
+
for shell in bash sh; do
|
|
53
|
+
if ! command -v "$shell" >/dev/null 2>&1; then
|
|
54
|
+
printf " SKIP %s not available\n" "$shell"
|
|
55
|
+
continue
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
result=$(run_wrapper "$shell" session-start)
|
|
59
|
+
rc="${result%%|*}"
|
|
60
|
+
out="${result#*|}"
|
|
61
|
+
|
|
62
|
+
if [ "$rc" -eq 124 ]; then
|
|
63
|
+
printf " FAIL %s: wrapper hung (timed out after %ss — self-exec loop?)\n" \
|
|
64
|
+
"$shell" "$TIMEOUT_SECS"
|
|
65
|
+
FAIL=$((FAIL + 1))
|
|
66
|
+
continue
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
if [ "$rc" -ne 0 ]; then
|
|
70
|
+
printf " FAIL %s: wrapper exited %s (expected 0)\n" "$shell" "$rc"
|
|
71
|
+
FAIL=$((FAIL + 1))
|
|
72
|
+
continue
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
if printf '%s' "$out" | python3 -c 'import json,sys; json.load(sys.stdin)' 2>/dev/null; then
|
|
76
|
+
printf " PASS %s: session-start dispatched, emitted valid JSON\n" "$shell"
|
|
77
|
+
PASS=$((PASS + 1))
|
|
78
|
+
else
|
|
79
|
+
printf " FAIL %s: output is not valid JSON: %.60s\n" "$shell" "$out"
|
|
80
|
+
FAIL=$((FAIL + 1))
|
|
81
|
+
fi
|
|
82
|
+
done
|
|
83
|
+
|
|
84
|
+
# --- Case 3: the dispatched hook is really session-start, not something else
|
|
85
|
+
result=$(run_wrapper bash session-start)
|
|
86
|
+
out="${result#*|}"
|
|
87
|
+
case "$out" in
|
|
88
|
+
*"Master-skill plugin loaded"*)
|
|
89
|
+
echo " PASS dispatches the named hook (context payload present)"
|
|
90
|
+
PASS=$((PASS + 1))
|
|
91
|
+
;;
|
|
92
|
+
*)
|
|
93
|
+
printf " FAIL dispatched hook did not produce session-start output: %.60s\n" "$out"
|
|
94
|
+
FAIL=$((FAIL + 1))
|
|
95
|
+
;;
|
|
96
|
+
esac
|
|
97
|
+
|
|
98
|
+
# --- Case 4: an unknown hook name fails loudly, never hangs --------------
|
|
99
|
+
result=$(run_wrapper bash no-such-hook)
|
|
100
|
+
rc="${result%%|*}"
|
|
101
|
+
if [ "$rc" -eq 124 ]; then
|
|
102
|
+
echo " FAIL unknown hook name hung instead of failing"
|
|
103
|
+
FAIL=$((FAIL + 1))
|
|
104
|
+
elif [ "$rc" -eq 0 ]; then
|
|
105
|
+
echo " FAIL unknown hook name reported success"
|
|
106
|
+
FAIL=$((FAIL + 1))
|
|
107
|
+
else
|
|
108
|
+
printf " PASS unknown hook name fails loudly (exit %s)\n" "$rc"
|
|
109
|
+
PASS=$((PASS + 1))
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
echo
|
|
113
|
+
printf "Summary: %d passed, %d failed\n" "$PASS" "$FAIL"
|
|
114
|
+
exit $([ "$FAIL" -eq 0 ] && echo 0 || echo 1)
|