master-skill 0.9.1 → 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 +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.cursor-plugin/plugin.json +2 -2
- package/ETHICS.md +23 -17
- package/README.md +57 -14
- package/README_EN.md +60 -16
- package/SKILL.md +5 -5
- package/bin/cli.mjs +497 -62
- package/gemini-extension.json +2 -2
- 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 +9 -3
- package/prebuilt/compare/SKILL.md +48 -14
- package/prebuilt/compare/tests/fidelity.jsonl +12 -12
- 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 +40 -10
- 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_fidelity.py +59 -0
- package/scripts/tests/test_validate_workflow.py +265 -0
- package/scripts/validate-citation-contract.py +193 -0
- package/scripts/validate-fidelity.py +20 -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.
|
|
@@ -68,19 +242,112 @@ function availableMasters() {
|
|
|
68
242
|
});
|
|
69
243
|
}
|
|
70
244
|
|
|
245
|
+
function readJson(filepath) {
|
|
246
|
+
return JSON.parse(fs.readFileSync(filepath, "utf8"));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function installedSkillDirs() {
|
|
250
|
+
if (!fs.existsSync(SKILLS_DIR)) return [];
|
|
251
|
+
return fs
|
|
252
|
+
.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
253
|
+
.filter((d) => d.isDirectory())
|
|
254
|
+
.map((d) => d.name)
|
|
255
|
+
.sort();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function hasLiveGrounding(masterDir) {
|
|
259
|
+
const skillMd = path.join(masterDir, "SKILL.md");
|
|
260
|
+
if (!fs.existsSync(skillMd)) return false;
|
|
261
|
+
const text = fs.readFileSync(skillMd, "utf8");
|
|
262
|
+
return text.includes("FoJin 实时检索") || text.includes("FoJin live");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function sourceIds(meta) {
|
|
266
|
+
if (!Array.isArray(meta.sources)) return [];
|
|
267
|
+
return meta.sources.map((s) => [s.id, s.title].filter(Boolean).join(" — "));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function printJson(payload) {
|
|
271
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
272
|
+
}
|
|
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
|
+
|
|
71
296
|
// --- commands ---
|
|
72
297
|
|
|
73
|
-
function
|
|
298
|
+
function listData() {
|
|
74
299
|
const masters = availableMasters();
|
|
75
|
-
|
|
76
|
-
|
|
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 });
|
|
305
|
+
return {
|
|
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
|
+
})),
|
|
315
|
+
masters: masters.map((m) => ({
|
|
316
|
+
name: m.name,
|
|
317
|
+
slug: m.name.replace(/^master-/, ""),
|
|
318
|
+
description: m.description,
|
|
319
|
+
})),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function cmdList({ json = false } = {}) {
|
|
324
|
+
const data = listData();
|
|
325
|
+
if (json) {
|
|
326
|
+
printJson(data);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const skills = data.skills;
|
|
331
|
+
if (!skills.length) {
|
|
332
|
+
console.log("No installable skills found.");
|
|
77
333
|
return;
|
|
78
334
|
}
|
|
79
|
-
console.log(`\nAvailable masters (${
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
}
|
|
84
351
|
}
|
|
85
352
|
console.log();
|
|
86
353
|
}
|
|
@@ -106,23 +373,37 @@ function cmdInstall(names) {
|
|
|
106
373
|
failed++;
|
|
107
374
|
continue;
|
|
108
375
|
}
|
|
109
|
-
const
|
|
110
|
-
if (!
|
|
111
|
-
console.log(` ✗ ${name} — not found in
|
|
376
|
+
const skill = resolveSkill(name);
|
|
377
|
+
if (!skill) {
|
|
378
|
+
console.log(` ✗ ${name} — not found in skill catalog`);
|
|
112
379
|
failed++;
|
|
113
380
|
continue;
|
|
114
381
|
}
|
|
115
|
-
const
|
|
116
|
-
const dest = path.join(SKILLS_DIR,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
}
|
|
121
392
|
console.log(` ✓ ${name} → ${dest}`);
|
|
122
393
|
}
|
|
123
394
|
return failed;
|
|
124
395
|
}
|
|
125
396
|
|
|
397
|
+
function cmdInstallAll(label = "Installing") {
|
|
398
|
+
const all = catalogSkills().map((skill) => skill.name);
|
|
399
|
+
if (!all.length) {
|
|
400
|
+
console.log("No skills available.");
|
|
401
|
+
return 1;
|
|
402
|
+
}
|
|
403
|
+
console.log(`${label} all ${all.length} skills...\n`);
|
|
404
|
+
return cmdInstall(all);
|
|
405
|
+
}
|
|
406
|
+
|
|
126
407
|
function cmdUninstall(names) {
|
|
127
408
|
let failed = 0;
|
|
128
409
|
for (const name of names) {
|
|
@@ -131,12 +412,16 @@ function cmdUninstall(names) {
|
|
|
131
412
|
failed++;
|
|
132
413
|
continue;
|
|
133
414
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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));
|
|
140
425
|
const dest = candidates.find((p) => fs.existsSync(p));
|
|
141
426
|
if (!dest) {
|
|
142
427
|
console.log(` ✗ ${name} — not installed`);
|
|
@@ -149,67 +434,217 @@ function cmdUninstall(names) {
|
|
|
149
434
|
return failed;
|
|
150
435
|
}
|
|
151
436
|
|
|
437
|
+
function doctorData() {
|
|
438
|
+
const masters = availableMasters();
|
|
439
|
+
const installed = installedSkillDirs();
|
|
440
|
+
const expectedInstalled = masters.filter((m) => installed.includes(m.name));
|
|
441
|
+
const missingSkillMd = masters.filter((m) => {
|
|
442
|
+
const masterDir = path.join(PREBUILT, m.name);
|
|
443
|
+
return !fs.existsSync(path.join(masterDir, "SKILL.md"));
|
|
444
|
+
});
|
|
445
|
+
const problems = missingSkillMd.map((m) => ({
|
|
446
|
+
code: "missing-skill-md",
|
|
447
|
+
name: m.name,
|
|
448
|
+
message: `${m.name} is missing SKILL.md`,
|
|
449
|
+
}));
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
packageVersion: pkgVersion(),
|
|
453
|
+
nodeVersion: process.version,
|
|
454
|
+
prebuiltPath: PREBUILT,
|
|
455
|
+
skillsPath: SKILLS_DIR,
|
|
456
|
+
availableSkills: masters.length,
|
|
457
|
+
installedKnownSkills: expectedInstalled.length,
|
|
458
|
+
otherInstalledSkillDirs: installed.length - expectedInstalled.length,
|
|
459
|
+
status: problems.length ? "problems" : "ok",
|
|
460
|
+
problems,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function cmdDoctor({ json = false } = {}) {
|
|
465
|
+
const data = doctorData();
|
|
466
|
+
if (json) {
|
|
467
|
+
printJson(data);
|
|
468
|
+
return data.problems.length ? 1 : 0;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
console.log(`master-skill doctor\n`);
|
|
472
|
+
console.log(`Package version: ${data.packageVersion}`);
|
|
473
|
+
console.log(`Node version: ${data.nodeVersion}`);
|
|
474
|
+
console.log(`Prebuilt path: ${data.prebuiltPath}`);
|
|
475
|
+
console.log(`Claude skills path: ${data.skillsPath}`);
|
|
476
|
+
console.log(`Available skills: ${data.availableSkills}`);
|
|
477
|
+
console.log(`Installed known skills: ${data.installedKnownSkills}`);
|
|
478
|
+
console.log(`Other installed skill dirs: ${data.otherInstalledSkillDirs}`);
|
|
479
|
+
|
|
480
|
+
if (data.problems.length) {
|
|
481
|
+
console.log(`\nProblems:`);
|
|
482
|
+
for (const problem of data.problems) {
|
|
483
|
+
console.log(` ✗ ${problem.message}`);
|
|
484
|
+
}
|
|
485
|
+
return 1;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
console.log(`\nStatus: ok`);
|
|
489
|
+
return 0;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function inspectData(name) {
|
|
493
|
+
const masterDir = resolveMasterDir(name);
|
|
494
|
+
if (!masterDir) return null;
|
|
495
|
+
|
|
496
|
+
const dirName = path.basename(masterDir);
|
|
497
|
+
const skillMd = path.join(masterDir, "SKILL.md");
|
|
498
|
+
const metaPath = path.join(masterDir, "meta.json");
|
|
499
|
+
const fm = fs.existsSync(skillMd) ? parseFrontmatter(skillMd) : {};
|
|
500
|
+
const meta = fs.existsSync(metaPath) ? readJson(metaPath) : {};
|
|
501
|
+
const sources = sourceIds(meta);
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
name: dirName,
|
|
505
|
+
displayName: meta.name || null,
|
|
506
|
+
slug: meta.slug || dirName.replace(/^master-/, ""),
|
|
507
|
+
version: fm.version || meta.version || null,
|
|
508
|
+
tradition: meta.tradition || null,
|
|
509
|
+
school: meta.school || null,
|
|
510
|
+
era: meta.era || null,
|
|
511
|
+
installed: fs.existsSync(path.join(SKILLS_DIR, dirName)),
|
|
512
|
+
liveGrounding: hasLiveGrounding(masterDir),
|
|
513
|
+
citationFormat: fm.citation_format || null,
|
|
514
|
+
sources,
|
|
515
|
+
searchKeywords: meta.search_scope?.keywords || [],
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function cmdInspect(name, { json = false } = {}) {
|
|
520
|
+
if (!name) {
|
|
521
|
+
console.log("Usage: master-skill inspect <name>");
|
|
522
|
+
return 1;
|
|
523
|
+
}
|
|
524
|
+
if (!isSafeName(name)) {
|
|
525
|
+
console.log(` ✗ ${name} — invalid name (letters, digits, "-", "_" only)`);
|
|
526
|
+
return 1;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const data = inspectData(name);
|
|
530
|
+
if (!data) {
|
|
531
|
+
console.log(` ✗ ${name} — not found in prebuilt/ (tried "${name}" and "master-${name}")`);
|
|
532
|
+
return 1;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (json) {
|
|
536
|
+
printJson(data);
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
console.log(`${data.name}\n`);
|
|
541
|
+
console.log(`Display name: ${data.displayName || "(none)"}`);
|
|
542
|
+
console.log(`Slug: ${data.slug}`);
|
|
543
|
+
console.log(`Version: ${data.version || "(unknown)"}`);
|
|
544
|
+
console.log(`Tradition: ${data.tradition || "(unspecified)"}`);
|
|
545
|
+
console.log(`School: ${data.school || "(unspecified)"}`);
|
|
546
|
+
console.log(`Era: ${data.era || "(unspecified)"}`);
|
|
547
|
+
console.log(`Installed: ${data.installed ? "yes" : "no"}`);
|
|
548
|
+
console.log(`Live grounding: ${data.liveGrounding ? "yes" : "no"}`);
|
|
549
|
+
console.log(`Citation format: ${data.citationFormat || "(not declared in SKILL frontmatter)"}`);
|
|
550
|
+
|
|
551
|
+
if (data.sources.length) {
|
|
552
|
+
console.log(`\nSources (${data.sources.length}):`);
|
|
553
|
+
for (const source of data.sources) console.log(` - ${source}`);
|
|
554
|
+
} else {
|
|
555
|
+
console.log(`\nSources: none declared in meta.json`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (data.searchKeywords.length) {
|
|
559
|
+
const keywords = data.searchKeywords.slice(0, 12).join(", ");
|
|
560
|
+
const suffix = data.searchKeywords.length > 12 ? ", ..." : "";
|
|
561
|
+
console.log(`\nSearch keywords: ${keywords}${suffix}`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return 0;
|
|
565
|
+
}
|
|
566
|
+
|
|
152
567
|
function showHelp() {
|
|
153
568
|
console.log(`
|
|
154
569
|
master-skill v${pkgVersion()} — Buddhist Master AI Skills installer
|
|
155
570
|
|
|
156
571
|
Usage:
|
|
157
|
-
master-skill install <name...> Install
|
|
158
|
-
master-skill install --all Install all available
|
|
159
|
-
master-skill
|
|
160
|
-
master-skill
|
|
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
|
|
577
|
+
master-skill inspect <name> Show source/runtime metadata for one master
|
|
578
|
+
master-skill inspect <name> --json
|
|
579
|
+
master-skill doctor Check local install and runtime paths
|
|
580
|
+
master-skill doctor --json Print diagnostics as JSON
|
|
581
|
+
master-skill uninstall <name...> Remove installed skills
|
|
161
582
|
master-skill --version Print version
|
|
162
583
|
master-skill --help Show this help
|
|
163
584
|
|
|
164
|
-
|
|
165
|
-
|
|
585
|
+
Persona names accept both short (zhiyi) and full (master-zhiyi) forms.
|
|
586
|
+
Teaching modes and create-master use their public catalog names.
|
|
166
587
|
|
|
167
588
|
Examples:
|
|
168
589
|
npx master-skill install zhiyi fazang
|
|
169
590
|
npx master-skill install master-milarepa master-tsongkhapa
|
|
591
|
+
npx master-skill install compare-masters
|
|
592
|
+
npx master-skill install create-master
|
|
170
593
|
npx master-skill install --all
|
|
594
|
+
npx master-skill update --all
|
|
171
595
|
npx master-skill list
|
|
596
|
+
npx master-skill inspect huineng
|
|
597
|
+
npx master-skill doctor
|
|
172
598
|
npx master-skill uninstall zhiyi
|
|
173
599
|
`);
|
|
174
600
|
}
|
|
175
601
|
|
|
176
602
|
// --- main ---
|
|
177
603
|
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
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");
|
|
193
626
|
process.exitCode = 1;
|
|
194
627
|
} else {
|
|
195
|
-
|
|
196
|
-
|
|
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;
|
|
197
645
|
}
|
|
198
|
-
} else if (rest.length === 0) {
|
|
199
|
-
console.log("Usage: master-skill install <name...> | --all");
|
|
200
|
-
process.exitCode = 1;
|
|
201
646
|
} else {
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
} else if (cmd === "uninstall") {
|
|
205
|
-
const rest = args.slice(1);
|
|
206
|
-
if (rest.length === 0) {
|
|
207
|
-
console.log("Usage: master-skill uninstall <name...>");
|
|
647
|
+
console.log(`Unknown command: ${cmd}\nRun master-skill --help for usage.`);
|
|
208
648
|
process.exitCode = 1;
|
|
209
|
-
} else {
|
|
210
|
-
if (cmdUninstall(rest) > 0) process.exitCode = 1;
|
|
211
649
|
}
|
|
212
|
-
} else {
|
|
213
|
-
console.log(`Unknown command: ${cmd}\nRun master-skill --help for usage.`);
|
|
214
|
-
process.exitCode = 1;
|
|
215
650
|
}
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "master-skill",
|
|
3
|
-
"description": "Buddhist
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "FoJin-powered Buddhist AI persona framework — source-grounded, boundary-aware, fidelity-tested, runtime-ready. 15 prebuilt masters across 印度/汉传/藏传/南传.",
|
|
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
|