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.
Files changed (88) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.cursor-plugin/plugin.json +2 -2
  4. package/ETHICS.md +23 -17
  5. package/README.md +57 -14
  6. package/README_EN.md +60 -16
  7. package/SKILL.md +5 -5
  8. package/bin/cli.mjs +497 -62
  9. package/gemini-extension.json +2 -2
  10. package/hooks/run-hook.cmd +18 -5
  11. package/hooks/tests/test_run_hook.sh +114 -0
  12. package/hooks/tests/test_run_hook_cmd.sh +94 -0
  13. package/masters/.gitkeep +0 -0
  14. package/package.json +9 -3
  15. package/prebuilt/compare/SKILL.md +48 -14
  16. package/prebuilt/compare/tests/fidelity.jsonl +12 -12
  17. package/prebuilt/master-ajahn-chah/SKILL.md +13 -11
  18. package/prebuilt/master-ajahn-chah/meta.json +8 -0
  19. package/prebuilt/master-ajahn-chah/references/voice.md +1 -1
  20. package/prebuilt/master-atisha/SKILL.md +13 -11
  21. package/prebuilt/master-atisha/meta.json +8 -0
  22. package/prebuilt/master-atisha/references/voice.md +1 -1
  23. package/prebuilt/master-buddhaghosa/SKILL.md +13 -11
  24. package/prebuilt/master-buddhaghosa/meta.json +8 -0
  25. package/prebuilt/master-buddhaghosa/references/voice.md +1 -1
  26. package/prebuilt/master-fazang/SKILL.md +3 -3
  27. package/prebuilt/master-fazang/meta.json +8 -0
  28. package/prebuilt/master-huineng/SKILL.md +3 -3
  29. package/prebuilt/master-huineng/meta.json +8 -0
  30. package/prebuilt/master-kumarajiva/SKILL.md +3 -3
  31. package/prebuilt/master-kumarajiva/meta.json +8 -0
  32. package/prebuilt/master-mahasi-sayadaw/SKILL.md +13 -11
  33. package/prebuilt/master-mahasi-sayadaw/meta.json +8 -0
  34. package/prebuilt/master-mahasi-sayadaw/references/voice.md +2 -2
  35. package/prebuilt/master-milarepa/SKILL.md +13 -11
  36. package/prebuilt/master-milarepa/meta.json +8 -0
  37. package/prebuilt/master-milarepa/references/voice.md +1 -1
  38. package/prebuilt/master-nagarjuna/SKILL.md +3 -3
  39. package/prebuilt/master-nagarjuna/meta.json +8 -0
  40. package/prebuilt/master-ouyi/SKILL.md +3 -3
  41. package/prebuilt/master-ouyi/meta.json +8 -0
  42. package/prebuilt/master-tsongkhapa/SKILL.md +13 -11
  43. package/prebuilt/master-tsongkhapa/meta.json +8 -0
  44. package/prebuilt/master-tsongkhapa/references/voice.md +1 -1
  45. package/prebuilt/master-xuanzang/SKILL.md +3 -3
  46. package/prebuilt/master-xuanzang/meta.json +8 -0
  47. package/prebuilt/master-xuyun/SKILL.md +3 -3
  48. package/prebuilt/master-xuyun/meta.json +8 -0
  49. package/prebuilt/master-yinguang/SKILL.md +3 -3
  50. package/prebuilt/master-yinguang/meta.json +8 -0
  51. package/prebuilt/master-zhiyi/SKILL.md +3 -3
  52. package/prebuilt/master-zhiyi/meta.json +8 -0
  53. package/prompts/correction_handler.md +104 -0
  54. package/prompts/doctrine_reviewer.md +61 -0
  55. package/prompts/intake.md +62 -0
  56. package/prompts/merger.md +62 -0
  57. package/prompts/rag_instructions.md +54 -0
  58. package/prompts/sutra_analyzer.md +83 -0
  59. package/prompts/teaching_builder.md +41 -0
  60. package/prompts/voice_analyzer.md +92 -0
  61. package/prompts/voice_builder.md +48 -0
  62. package/prompts/voice_reviewer.md +66 -0
  63. package/references/README.md +12 -0
  64. package/references/ethics-runtime.md +112 -0
  65. package/references/fojin-api.md +223 -0
  66. package/references/source-conventions.md +129 -0
  67. package/references/teaching-modes.md +84 -0
  68. package/references/traditions.md +72 -0
  69. package/references/workflow-details.md +361 -0
  70. package/requirements.txt +6 -0
  71. package/scripts/select-fidelity-smoke.py +78 -0
  72. package/scripts/test-fidelity.py +40 -10
  73. package/scripts/tests/test_select_fidelity_smoke.py +142 -0
  74. package/scripts/tests/test_validate_citation_contract.py +408 -0
  75. package/scripts/tests/test_validate_fidelity.py +59 -0
  76. package/scripts/tests/test_validate_workflow.py +265 -0
  77. package/scripts/validate-citation-contract.py +193 -0
  78. package/scripts/validate-fidelity.py +20 -0
  79. package/scripts/verify_citations.py +8 -1
  80. package/skill-catalog.json +147 -0
  81. package/tools/cross_reference.py +365 -0
  82. package/tools/fojin_bridge.py +146 -0
  83. package/tools/master_builder.py +341 -0
  84. package/tools/rag_query.py +336 -0
  85. package/tools/skill_writer.py +230 -0
  86. package/tools/sutra_collector.py +237 -0
  87. package/tools/verify_sources.py +512 -0
  88. 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 PREBUILT = path.join(__dirname, "..", "prebuilt");
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 cmdList() {
298
+ function listData() {
74
299
  const masters = availableMasters();
75
- if (!masters.length) {
76
- console.log("No prebuilt masters found.");
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 (${masters.length}):\n`);
80
- const nameW = Math.max(...masters.map((m) => m.name.length), 4);
81
- for (const m of masters) {
82
- const desc = m.description.length > 80 ? m.description.slice(0, 77) + "..." : m.description;
83
- console.log(` ${m.name.padEnd(nameW)} ${desc}`);
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 src = resolveMasterDir(name);
110
- if (!src) {
111
- console.log(` ✗ ${name} — not found in prebuilt/ (tried "${name}" and "master-${name}")`);
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 dirName = path.basename(src); // master-zhiyi
116
- const dest = path.join(SKILLS_DIR, dirName);
117
- // Clear any previous install first: files renamed or removed upstream
118
- // must not linger as stale skill content under ~/.claude/skills/.
119
- fs.rmSync(dest, { recursive: true, force: true });
120
- cpR(src, dest);
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
- // Try both prefixed and bare directory names for backward compatibility
135
- // with any pre-v0.6 installs that may still sit at ~/.claude/skills/<slug>/.
136
- const candidates = [
137
- path.join(SKILLS_DIR, name), // exact: master-zhiyi
138
- path.join(SKILLS_DIR, `master-${name}`), // short: zhiyi → master-zhiyi
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 masters to ~/.claude/skills/
158
- master-skill install --all Install all available masters
159
- master-skill list List available masters
160
- master-skill uninstall <name...> Remove installed masters
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
- Names accept both short (zhiyi) and full (master-zhiyi) forms.
165
- Slash commands are always /master-<slug> (e.g. /master-zhiyi).
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
- const args = process.argv.slice(2);
179
- const cmd = args[0];
180
-
181
- if (!cmd || cmd === "--help" || cmd === "-h") {
182
- showHelp();
183
- } else if (cmd === "--version" || cmd === "-v") {
184
- console.log(pkgVersion());
185
- } else if (cmd === "list") {
186
- cmdList();
187
- } else if (cmd === "install") {
188
- const rest = args.slice(1);
189
- if (rest.includes("--all")) {
190
- const all = availableMasters().map((m) => m.name);
191
- if (!all.length) {
192
- console.log("No masters available.");
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
- console.log(`Installing all ${all.length} masters...\n`);
196
- if (cmdInstall(all) > 0) process.exitCode = 1;
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
- if (cmdInstall(rest) > 0) process.exitCode = 1;
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "master-skill",
3
- "description": "Buddhist Master AI teaching personas14 prebuilt masters across 汉传/藏传/南传 invokable via /master-<slug> slash commands, with source-cited doctrinal responses (CBETA / BDRC / SuttaCentral / PTS Vism)",
4
- "version": "0.9.1",
3
+ "description": "FoJin-powered Buddhist AI persona frameworksource-grounded, boundary-aware, fidelity-tested, runtime-ready. 15 prebuilt masters across 印度/汉传/藏传/南传.",
4
+ "version": "0.10.1",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -1,6 +1,15 @@
1
- : ; # Polyglot wrapper — runs as cmd.exe on Windows, bash on Unix
2
- : ; exec bash "$0" "$@" 2>/dev/null
3
- : ; exit
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
- exit /b %ERRORLEVEL%
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 %ERRORLEVEL%
38
+ exit /b !ERRORLEVEL!
26
39
  )
27
40
 
28
41
  :: No bash found — exit silently