master-skill 0.10.0 → 0.11.0
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/GEMINI.md +1 -1
- package/README.md +57 -295
- package/README_EN.md +59 -276
- package/SKILL.md +5 -5
- package/bin/cli.mjs +545 -78
- package/gemini-extension.json +1 -1
- package/hooks/run-hook.cmd +18 -5
- package/hooks/session-start +4 -1
- 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 +10 -3
- package/prebuilt/{compare → compare-masters}/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-curriculum/SKILL.md +1 -1
- package/prebuilt/master-debate/SKILL.md +1 -1
- package/prebuilt/master-fazang/SKILL.md +3 -3
- package/prebuilt/master-fazang/meta.json +8 -0
- package/prebuilt/master-help/SKILL.md +86 -0
- package/prebuilt/master-help/tests/fidelity.jsonl +10 -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 +20 -1
- 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 +25 -2
- 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 +32 -3
- 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 +91 -0
- package/references/traditions.md +72 -0
- package/references/workflow-details.md +361 -0
- package/requirements.txt +6 -0
- package/routing.json +209 -0
- package/scripts/check-gate-liveness.py +222 -0
- package/scripts/select-fidelity-smoke.py +78 -0
- package/scripts/test-fidelity.py +339 -51
- package/scripts/tests/test_check_gate_liveness.py +232 -0
- package/scripts/tests/test_check_response.py +190 -0
- package/scripts/tests/test_fidelity_providers.py +202 -0
- package/scripts/tests/test_select_fidelity_smoke.py +142 -0
- package/scripts/tests/test_validate.py +145 -0
- package/scripts/tests/test_validate_citation_contract.py +408 -0
- package/scripts/tests/test_validate_fidelity.py +2 -2
- package/scripts/tests/test_validate_workflow.py +284 -0
- package/scripts/validate-citation-contract.py +193 -0
- package/scripts/validate-fidelity.py +6 -1
- package/scripts/validate-routing.py +254 -0
- package/scripts/validate.py +63 -36
- package/scripts/verify_citations.py +8 -1
- package/skill-catalog.json +210 -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/prebuilt/{compare → compare-masters}/tests/fidelity.jsonl +0 -0
package/bin/cli.mjs
CHANGED
|
@@ -8,8 +8,12 @@ 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");
|
|
14
|
+
const ROUTING_PATH = path.join(PACKAGE_ROOT, "routing.json");
|
|
12
15
|
const SKILLS_DIR = path.join(os.homedir(), ".claude", "skills");
|
|
16
|
+
const SKILL_KINDS = new Set(["persona", "teaching-mode", "generator"]);
|
|
13
17
|
|
|
14
18
|
// --- helpers ---
|
|
15
19
|
|
|
@@ -27,7 +31,119 @@ function pkgVersion() {
|
|
|
27
31
|
// Names feed into path.join + rmSync; reject separators / ".." so a typo
|
|
28
32
|
// like "../foo" can never escape PREBUILT or SKILLS_DIR.
|
|
29
33
|
function isSafeName(name) {
|
|
30
|
-
return /^[A-Za-z0-9_-]+$/.test(name);
|
|
34
|
+
return typeof name === "string" && /^[A-Za-z0-9_-]+$/.test(name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isSafeRelativePath(value, { allowDot = false } = {}) {
|
|
38
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
39
|
+
if (allowDot && value === ".") return true;
|
|
40
|
+
if (value === "." || value.includes("\\") || path.posix.isAbsolute(value)) return false;
|
|
41
|
+
if (/^[A-Za-z]:/.test(value)) return false;
|
|
42
|
+
const normalized = path.posix.normalize(value);
|
|
43
|
+
return normalized === value && normalized !== ".." && !normalized.startsWith("../");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function invalidCatalog(message) {
|
|
47
|
+
throw new Error(`Invalid skill catalog: ${message}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadCatalog() {
|
|
51
|
+
let catalog;
|
|
52
|
+
try {
|
|
53
|
+
catalog = JSON.parse(fs.readFileSync(CATALOG_PATH, "utf8"));
|
|
54
|
+
} catch (err) {
|
|
55
|
+
invalidCatalog(`cannot read ${path.basename(CATALOG_PATH)} (${err.message})`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (catalog?.version !== 1) invalidCatalog("version must be 1");
|
|
59
|
+
if (!Array.isArray(catalog.skills) || catalog.skills.length === 0) {
|
|
60
|
+
invalidCatalog("skills must be a non-empty array");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const uniqueFields = {
|
|
64
|
+
name: new Set(),
|
|
65
|
+
source: new Set(),
|
|
66
|
+
install_dir: new Set(),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
for (const [index, skill] of catalog.skills.entries()) {
|
|
70
|
+
if (!skill || typeof skill !== "object" || Array.isArray(skill)) {
|
|
71
|
+
invalidCatalog(`skills[${index}] must be an object`);
|
|
72
|
+
}
|
|
73
|
+
if (!isSafeName(skill.name)) {
|
|
74
|
+
invalidCatalog(`skills[${index}].name must be a safe non-empty string`);
|
|
75
|
+
}
|
|
76
|
+
if (!SKILL_KINDS.has(skill.kind)) {
|
|
77
|
+
invalidCatalog(`skills[${index}].kind must be persona, teaching-mode, or generator`);
|
|
78
|
+
}
|
|
79
|
+
if (!isSafeRelativePath(skill.source, { allowDot: true })) {
|
|
80
|
+
invalidCatalog(`skills[${index}].source must be a safe relative path`);
|
|
81
|
+
}
|
|
82
|
+
if (!isSafeName(skill.install_dir)) {
|
|
83
|
+
invalidCatalog(`skills[${index}].install_dir must be a safe non-empty string`);
|
|
84
|
+
}
|
|
85
|
+
if (!Array.isArray(skill.aliases)) {
|
|
86
|
+
invalidCatalog(`skills[${index}].aliases must be an array`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const field of Object.keys(uniqueFields)) {
|
|
90
|
+
if (uniqueFields[field].has(skill[field])) {
|
|
91
|
+
invalidCatalog(`duplicate ${field} "${skill[field]}"`);
|
|
92
|
+
}
|
|
93
|
+
uniqueFields[field].add(skill[field]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sourcePath = path.resolve(PACKAGE_ROOT, skill.source);
|
|
97
|
+
if (!fs.existsSync(sourcePath)) invalidCatalog(`source does not exist: ${skill.source}`);
|
|
98
|
+
if (!fs.statSync(sourcePath).isDirectory()) {
|
|
99
|
+
invalidCatalog(`source is not a directory: ${skill.source}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (skill.kind === "generator") {
|
|
103
|
+
if (!Array.isArray(skill.bundle_paths) || skill.bundle_paths.length === 0) {
|
|
104
|
+
invalidCatalog(`generator "${skill.name}" must declare bundle_paths`);
|
|
105
|
+
}
|
|
106
|
+
const bundlePaths = new Set();
|
|
107
|
+
for (const bundlePath of skill.bundle_paths) {
|
|
108
|
+
if (!isSafeRelativePath(bundlePath)) {
|
|
109
|
+
invalidCatalog(`generator "${skill.name}" has an unsafe bundle path`);
|
|
110
|
+
}
|
|
111
|
+
if (bundlePaths.has(bundlePath)) {
|
|
112
|
+
invalidCatalog(`generator "${skill.name}" has duplicate bundle path "${bundlePath}"`);
|
|
113
|
+
}
|
|
114
|
+
bundlePaths.add(bundlePath);
|
|
115
|
+
if (!fs.existsSync(path.resolve(sourcePath, bundlePath))) {
|
|
116
|
+
invalidCatalog(`bundle path does not exist: ${bundlePath}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} else if (skill.bundle_paths !== undefined) {
|
|
120
|
+
invalidCatalog(`only generator skills may declare bundle_paths`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Register every canonical name before considering aliases so token
|
|
125
|
+
// ownership is independent of catalog record order. An alias may repeat its
|
|
126
|
+
// own canonical name, but it may never shadow another skill's canonical name.
|
|
127
|
+
const aliases = new Set();
|
|
128
|
+
const tokenOwners = new Map(
|
|
129
|
+
catalog.skills.map((skill) => [skill.name, skill.name])
|
|
130
|
+
);
|
|
131
|
+
for (const [index, skill] of catalog.skills.entries()) {
|
|
132
|
+
for (const alias of skill.aliases) {
|
|
133
|
+
if (!isSafeName(alias)) {
|
|
134
|
+
invalidCatalog(`skills[${index}].aliases contains an unsafe value`);
|
|
135
|
+
}
|
|
136
|
+
if (aliases.has(alias)) invalidCatalog(`duplicate alias "${alias}"`);
|
|
137
|
+
aliases.add(alias);
|
|
138
|
+
const existingOwner = tokenOwners.get(alias);
|
|
139
|
+
if (existingOwner && existingOwner !== skill.name) {
|
|
140
|
+
invalidCatalog(`alias "${alias}" conflicts with skill "${existingOwner}"`);
|
|
141
|
+
}
|
|
142
|
+
tokenOwners.set(alias, skill.name);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return catalog;
|
|
31
147
|
}
|
|
32
148
|
|
|
33
149
|
function cpR(src, dest) {
|
|
@@ -40,6 +156,65 @@ function cpR(src, dest) {
|
|
|
40
156
|
}
|
|
41
157
|
}
|
|
42
158
|
|
|
159
|
+
function copyGeneratorBundle(skill, src, dest) {
|
|
160
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
161
|
+
for (const bundlePath of skill.bundle_paths) {
|
|
162
|
+
const bundleSource = path.join(src, bundlePath);
|
|
163
|
+
const bundleDest = path.join(dest, bundlePath);
|
|
164
|
+
if (fs.statSync(bundleSource).isDirectory()) {
|
|
165
|
+
cpR(bundleSource, bundleDest);
|
|
166
|
+
} else {
|
|
167
|
+
fs.mkdirSync(path.dirname(bundleDest), { recursive: true });
|
|
168
|
+
fs.copyFileSync(bundleSource, bundleDest);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function replaceGeneratorInstall(skill, src, dest) {
|
|
174
|
+
const staging = fs.mkdtempSync(
|
|
175
|
+
path.join(SKILLS_DIR, `.${skill.install_dir}-staging-`)
|
|
176
|
+
);
|
|
177
|
+
let backup = null;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
copyGeneratorBundle(skill, src, staging);
|
|
181
|
+
|
|
182
|
+
// Generated personas are user data, not package runtime. Merge them into
|
|
183
|
+
// the staged bundle before replacing the old install so reinstall/update
|
|
184
|
+
// can still remove stale runtime files without erasing masters/*.
|
|
185
|
+
const existingMasters = path.join(dest, "masters");
|
|
186
|
+
if (fs.existsSync(existingMasters)) {
|
|
187
|
+
cpR(existingMasters, path.join(staging, "masters"));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (fs.existsSync(dest)) {
|
|
191
|
+
backup = fs.mkdtempSync(
|
|
192
|
+
path.join(SKILLS_DIR, `.${skill.install_dir}-backup-`)
|
|
193
|
+
);
|
|
194
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
195
|
+
fs.renameSync(dest, backup);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
fs.renameSync(staging, dest);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (backup && fs.existsSync(backup) && !fs.existsSync(dest)) {
|
|
202
|
+
fs.renameSync(backup, dest);
|
|
203
|
+
backup = null;
|
|
204
|
+
}
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (backup) {
|
|
209
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
210
|
+
backup = null;
|
|
211
|
+
}
|
|
212
|
+
} finally {
|
|
213
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
214
|
+
if (backup) fs.rmSync(backup, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
43
218
|
function parseFrontmatter(filepath) {
|
|
44
219
|
// \r?\n: a CRLF checkout (git autocrlf on Windows) must not blank out
|
|
45
220
|
// every description.
|
|
@@ -60,7 +235,7 @@ function availableMasters() {
|
|
|
60
235
|
if (!fs.existsSync(PREBUILT)) return [];
|
|
61
236
|
return fs
|
|
62
237
|
.readdirSync(PREBUILT, { withFileTypes: true })
|
|
63
|
-
.filter((d) => d.isDirectory() && d.name !== "compare")
|
|
238
|
+
.filter((d) => d.isDirectory() && d.name !== "compare-masters")
|
|
64
239
|
.map((d) => {
|
|
65
240
|
const skillMd = path.join(PREBUILT, d.name, "SKILL.md");
|
|
66
241
|
const fm = fs.existsSync(skillMd) ? parseFrontmatter(skillMd) : {};
|
|
@@ -97,12 +272,47 @@ function printJson(payload) {
|
|
|
97
272
|
console.log(JSON.stringify(payload, null, 2));
|
|
98
273
|
}
|
|
99
274
|
|
|
275
|
+
let CATALOG;
|
|
276
|
+
try {
|
|
277
|
+
CATALOG = loadCatalog();
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.error(err.message);
|
|
280
|
+
process.exitCode = 1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function catalogSkills() {
|
|
284
|
+
return CATALOG.skills.map((skill) => {
|
|
285
|
+
const skillMd = path.join(PACKAGE_ROOT, skill.source, "SKILL.md");
|
|
286
|
+
const fm = fs.existsSync(skillMd) ? parseFrontmatter(skillMd) : {};
|
|
287
|
+
return { ...skill, description: fm.description || "" };
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function resolveSkill(input) {
|
|
292
|
+
return catalogSkills().find(
|
|
293
|
+
(skill) => skill.name === input || skill.aliases.includes(input)
|
|
294
|
+
) || null;
|
|
295
|
+
}
|
|
296
|
+
|
|
100
297
|
// --- commands ---
|
|
101
298
|
|
|
102
299
|
function listData() {
|
|
103
300
|
const masters = availableMasters();
|
|
301
|
+
const skills = catalogSkills();
|
|
302
|
+
const categoryCounts = skills.reduce((counts, skill) => {
|
|
303
|
+
counts[skill.kind] += 1;
|
|
304
|
+
return counts;
|
|
305
|
+
}, { persona: 0, "teaching-mode": 0, generator: 0 });
|
|
104
306
|
return {
|
|
105
307
|
count: masters.length,
|
|
308
|
+
skillCount: skills.length,
|
|
309
|
+
categoryCounts,
|
|
310
|
+
skills: skills.map(({ name, kind, install_dir, description }) => ({
|
|
311
|
+
name,
|
|
312
|
+
kind,
|
|
313
|
+
installDir: install_dir,
|
|
314
|
+
description,
|
|
315
|
+
})),
|
|
106
316
|
masters: masters.map((m) => ({
|
|
107
317
|
name: m.name,
|
|
108
318
|
slug: m.name.replace(/^master-/, ""),
|
|
@@ -118,16 +328,27 @@ function cmdList({ json = false } = {}) {
|
|
|
118
328
|
return;
|
|
119
329
|
}
|
|
120
330
|
|
|
121
|
-
const
|
|
122
|
-
if (!
|
|
123
|
-
console.log("No
|
|
331
|
+
const skills = data.skills;
|
|
332
|
+
if (!skills.length) {
|
|
333
|
+
console.log("No installable skills found.");
|
|
124
334
|
return;
|
|
125
335
|
}
|
|
126
|
-
console.log(`\nAvailable masters (${data.count})
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
336
|
+
console.log(`\nAvailable masters (${data.count}); installable skills (${data.skillCount}):`);
|
|
337
|
+
const groups = [
|
|
338
|
+
["persona", "Personas"],
|
|
339
|
+
["teaching-mode", "Teaching modes"],
|
|
340
|
+
["generator", "Generator"],
|
|
341
|
+
];
|
|
342
|
+
const nameW = Math.max(...skills.map((skill) => skill.name.length), 4);
|
|
343
|
+
for (const [kind, label] of groups) {
|
|
344
|
+
const group = skills.filter((skill) => skill.kind === kind);
|
|
345
|
+
console.log(`\n${label} (${group.length}):`);
|
|
346
|
+
for (const skill of group) {
|
|
347
|
+
const desc = skill.description.length > 80
|
|
348
|
+
? skill.description.slice(0, 77) + "..."
|
|
349
|
+
: skill.description;
|
|
350
|
+
console.log(` ${skill.name.padEnd(nameW)} ${desc}`);
|
|
351
|
+
}
|
|
131
352
|
}
|
|
132
353
|
console.log();
|
|
133
354
|
}
|
|
@@ -153,30 +374,34 @@ function cmdInstall(names) {
|
|
|
153
374
|
failed++;
|
|
154
375
|
continue;
|
|
155
376
|
}
|
|
156
|
-
const
|
|
157
|
-
if (!
|
|
158
|
-
console.log(` ✗ ${name} — not found in
|
|
377
|
+
const skill = resolveSkill(name);
|
|
378
|
+
if (!skill) {
|
|
379
|
+
console.log(` ✗ ${name} — not found in skill catalog`);
|
|
159
380
|
failed++;
|
|
160
381
|
continue;
|
|
161
382
|
}
|
|
162
|
-
const
|
|
163
|
-
const dest = path.join(SKILLS_DIR,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
383
|
+
const src = path.join(PACKAGE_ROOT, skill.source);
|
|
384
|
+
const dest = path.join(SKILLS_DIR, skill.install_dir);
|
|
385
|
+
if (skill.kind === "generator") {
|
|
386
|
+
replaceGeneratorInstall(skill, src, dest);
|
|
387
|
+
} else {
|
|
388
|
+
// Clear any previous install first: files renamed or removed upstream
|
|
389
|
+
// must not linger as stale skill content under ~/.claude/skills/.
|
|
390
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
391
|
+
cpR(src, dest);
|
|
392
|
+
}
|
|
168
393
|
console.log(` ✓ ${name} → ${dest}`);
|
|
169
394
|
}
|
|
170
395
|
return failed;
|
|
171
396
|
}
|
|
172
397
|
|
|
173
398
|
function cmdInstallAll(label = "Installing") {
|
|
174
|
-
const all =
|
|
399
|
+
const all = catalogSkills().map((skill) => skill.name);
|
|
175
400
|
if (!all.length) {
|
|
176
|
-
console.log("No
|
|
401
|
+
console.log("No skills available.");
|
|
177
402
|
return 1;
|
|
178
403
|
}
|
|
179
|
-
console.log(`${label} all ${all.length}
|
|
404
|
+
console.log(`${label} all ${all.length} skills...\n`);
|
|
180
405
|
return cmdInstall(all);
|
|
181
406
|
}
|
|
182
407
|
|
|
@@ -188,12 +413,16 @@ function cmdUninstall(names) {
|
|
|
188
413
|
failed++;
|
|
189
414
|
continue;
|
|
190
415
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
416
|
+
const skill = resolveSkill(name);
|
|
417
|
+
if (!skill) {
|
|
418
|
+
console.log(` ✗ ${name} — not found in skill catalog`);
|
|
419
|
+
failed++;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
// Keep removing legacy bare persona directories when they exist, while
|
|
423
|
+
// resolving every public name through the catalog's canonical target.
|
|
424
|
+
const candidates = [path.join(SKILLS_DIR, skill.install_dir)];
|
|
425
|
+
if (name !== skill.install_dir) candidates.push(path.join(SKILLS_DIR, name));
|
|
197
426
|
const dest = candidates.find((p) => fs.existsSync(p));
|
|
198
427
|
if (!dest) {
|
|
199
428
|
console.log(` ✗ ${name} — not installed`);
|
|
@@ -336,34 +565,266 @@ function cmdInspect(name, { json = false } = {}) {
|
|
|
336
565
|
return 0;
|
|
337
566
|
}
|
|
338
567
|
|
|
568
|
+
// --- recommend ---
|
|
569
|
+
//
|
|
570
|
+
// Routing used to exist only as prose (a weighted-match paragraph and a
|
|
571
|
+
// pairing table in prebuilt/compare-masters/SKILL.md, a decision tree in
|
|
572
|
+
// references/teaching-modes.md), so nothing could execute or test it. This
|
|
573
|
+
// reads routing.json for the parts that had no machine-readable home and
|
|
574
|
+
// scores personas straight off each meta.json search_scope.keywords, which
|
|
575
|
+
// stays the single source of truth for keywords.
|
|
576
|
+
//
|
|
577
|
+
// Only exact keyword containment scores. The prose also described "related
|
|
578
|
+
// match = 2" and "weak match = 1" tiers, but those need a synonym/domain
|
|
579
|
+
// map that does not exist — implementing them would dress a guess up as an
|
|
580
|
+
// algorithm. Ties break on tradition diversity, then slug order, so the
|
|
581
|
+
// same query always yields the same answer.
|
|
582
|
+
|
|
583
|
+
function loadRouting() {
|
|
584
|
+
const routing = JSON.parse(fs.readFileSync(ROUTING_PATH, "utf8"));
|
|
585
|
+
if (routing?.version !== 1) {
|
|
586
|
+
throw new Error("Invalid routing table: version must be 1");
|
|
587
|
+
}
|
|
588
|
+
return routing;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function personaCandidates() {
|
|
592
|
+
return CATALOG.skills
|
|
593
|
+
.filter((skill) => skill.kind === "persona")
|
|
594
|
+
.map((skill) => {
|
|
595
|
+
const metaPath = path.join(PACKAGE_ROOT, skill.source, "meta.json");
|
|
596
|
+
const meta = fs.existsSync(metaPath) ? readJson(metaPath) : {};
|
|
597
|
+
return {
|
|
598
|
+
name: skill.name,
|
|
599
|
+
tradition: meta.tradition || "(unspecified)",
|
|
600
|
+
keywords: meta.search_scope?.keywords || [],
|
|
601
|
+
};
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Greedy pick: highest score first, then prefer a tradition not yet chosen
|
|
606
|
+
// so the result shows plural perspectives rather than three Chan masters.
|
|
607
|
+
function pickDiverse(scored, limit) {
|
|
608
|
+
const pool = [...scored];
|
|
609
|
+
const chosen = [];
|
|
610
|
+
const seenTraditions = new Set();
|
|
611
|
+
while (pool.length && chosen.length < limit) {
|
|
612
|
+
let idx = pool.findIndex((c) => !seenTraditions.has(c.tradition));
|
|
613
|
+
if (idx === -1) idx = 0;
|
|
614
|
+
const [pick] = pool.splice(idx, 1);
|
|
615
|
+
chosen.push(pick);
|
|
616
|
+
seenTraditions.add(pick.tradition);
|
|
617
|
+
}
|
|
618
|
+
return chosen;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Shared by the situations and topic_pairings layers. Keyword sets are
|
|
622
|
+
// pairwise disjoint within each section (enforced by validate-routing.py),
|
|
623
|
+
// but a query can still touch two rows through different keywords — so the
|
|
624
|
+
// tiebreak is explicit and total: most hits, then longest single hit, then
|
|
625
|
+
// row id, which leaves no room for iteration order to decide.
|
|
626
|
+
function pickRow(rows, hitsFor) {
|
|
627
|
+
return (
|
|
628
|
+
rows
|
|
629
|
+
.map((row) => ({ row, matched: hitsFor(row.keywords) }))
|
|
630
|
+
.filter((entry) => entry.matched.length)
|
|
631
|
+
.sort(
|
|
632
|
+
(a, b) =>
|
|
633
|
+
b.matched.length - a.matched.length ||
|
|
634
|
+
Math.max(...b.matched.map((k) => k.length)) -
|
|
635
|
+
Math.max(...a.matched.map((k) => k.length)) ||
|
|
636
|
+
a.row.id.localeCompare(b.row.id)
|
|
637
|
+
)[0] || null
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function expandSlugs(slugs) {
|
|
642
|
+
const byName = new Map(personaCandidates().map((c) => [c.name, c]));
|
|
643
|
+
return slugs.map((name) => ({
|
|
644
|
+
name,
|
|
645
|
+
command: `/${name}`,
|
|
646
|
+
tradition: byName.get(name)?.tradition || "(unspecified)",
|
|
647
|
+
score: 0,
|
|
648
|
+
matched: [],
|
|
649
|
+
}));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function recommendData(query) {
|
|
653
|
+
const routing = loadRouting();
|
|
654
|
+
const q = String(query).toLowerCase();
|
|
655
|
+
const hitsFor = (keywords) =>
|
|
656
|
+
keywords.filter((kw) => q.includes(String(kw).toLowerCase()));
|
|
657
|
+
|
|
658
|
+
// Priority 1 — teaching mode, short-circuited in declared order.
|
|
659
|
+
for (const rule of [...routing.mode_rules].sort((a, b) => a.order - b.order)) {
|
|
660
|
+
const matched = hitsFor(rule.keywords);
|
|
661
|
+
if (matched.length) {
|
|
662
|
+
return {
|
|
663
|
+
query,
|
|
664
|
+
resolvedBy: "mode_rules",
|
|
665
|
+
kind: "teaching-mode",
|
|
666
|
+
mode: rule.mode,
|
|
667
|
+
command: `/${rule.mode}`,
|
|
668
|
+
matched,
|
|
669
|
+
note: rule.note || null,
|
|
670
|
+
masters: [],
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Priority 2 — score personas off their own declared keywords. Keywords
|
|
676
|
+
// below min_keyword_length are skipped: see the note in routing.json.
|
|
677
|
+
const weight = routing.weights?.keyword_hit ?? 3;
|
|
678
|
+
const minLen = routing.min_keyword_length ?? 2;
|
|
679
|
+
const scored = personaCandidates()
|
|
680
|
+
.map((c) => {
|
|
681
|
+
const matched = hitsFor(c.keywords.filter((kw) => String(kw).length >= minLen));
|
|
682
|
+
return { ...c, matched, score: matched.length * weight };
|
|
683
|
+
})
|
|
684
|
+
.filter((c) => c.score > 0)
|
|
685
|
+
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
686
|
+
|
|
687
|
+
if (scored.length) {
|
|
688
|
+
return {
|
|
689
|
+
query,
|
|
690
|
+
resolvedBy: "persona_keywords",
|
|
691
|
+
kind: "persona",
|
|
692
|
+
mode: null,
|
|
693
|
+
command: null,
|
|
694
|
+
matched: [],
|
|
695
|
+
note: null,
|
|
696
|
+
masters: pickDiverse(scored, 3).map((c) => ({
|
|
697
|
+
name: c.name,
|
|
698
|
+
command: `/${c.name}`,
|
|
699
|
+
tradition: c.tradition,
|
|
700
|
+
score: c.score,
|
|
701
|
+
matched: c.matched,
|
|
702
|
+
})),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Priority 3 — vernacular felt-state. A beginner types 坐不住, not 四念处,
|
|
707
|
+
// and search_scope.keywords carry no such wording, so these queries used to
|
|
708
|
+
// land on the default pairing. Placed after keyword scoring (an explicit
|
|
709
|
+
// doctrinal term is a stronger signal) and before topic_pairings (which was
|
|
710
|
+
// authored to pair masters for /compare-masters, not to answer "ask who?").
|
|
711
|
+
const situation = pickRow(routing.situations || [], hitsFor);
|
|
712
|
+
if (situation) {
|
|
713
|
+
return {
|
|
714
|
+
query,
|
|
715
|
+
resolvedBy: "situations",
|
|
716
|
+
kind: "persona",
|
|
717
|
+
mode: null,
|
|
718
|
+
command: null,
|
|
719
|
+
matched: situation.matched,
|
|
720
|
+
note: situation.row.note || null,
|
|
721
|
+
masters: expandSlugs(situation.row.masters),
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Priority 4 — topic pairing fallback.
|
|
726
|
+
const pairing = pickRow(routing.topic_pairings, hitsFor);
|
|
727
|
+
if (pairing) {
|
|
728
|
+
return {
|
|
729
|
+
query,
|
|
730
|
+
resolvedBy: "topic_pairings",
|
|
731
|
+
kind: "persona",
|
|
732
|
+
mode: null,
|
|
733
|
+
command: null,
|
|
734
|
+
matched: pairing.matched,
|
|
735
|
+
note: pairing.row.note || null,
|
|
736
|
+
masters: expandSlugs(pairing.row.masters),
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Priority 5 — nothing matched at all.
|
|
741
|
+
return {
|
|
742
|
+
query,
|
|
743
|
+
resolvedBy: "default_pairing",
|
|
744
|
+
kind: "persona",
|
|
745
|
+
mode: null,
|
|
746
|
+
command: null,
|
|
747
|
+
matched: [],
|
|
748
|
+
note: "无关键词命中,回退到默认配对",
|
|
749
|
+
masters: expandSlugs(routing.default_pairing),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function cmdRecommend(query, { json = false } = {}) {
|
|
754
|
+
if (!query || !String(query).trim()) {
|
|
755
|
+
console.log('Usage: master-skill recommend "<你的问题或状况>"');
|
|
756
|
+
return 1;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const data = recommendData(query);
|
|
760
|
+
|
|
761
|
+
if (json) {
|
|
762
|
+
printJson(data);
|
|
763
|
+
return 0;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (data.kind === "teaching-mode") {
|
|
767
|
+
console.log(`\n建议使用教学模式:${data.command}`);
|
|
768
|
+
if (data.note) console.log(` ${data.note}`);
|
|
769
|
+
console.log(` 命中关键词:${data.matched.join("、")}`);
|
|
770
|
+
console.log(
|
|
771
|
+
`\n(若只想听一位祖师,直接用对应的 /master-<name>;` +
|
|
772
|
+
`master-skill list 可列出全部。)\n`
|
|
773
|
+
);
|
|
774
|
+
return 0;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
console.log(`\n推荐祖师:`);
|
|
778
|
+
for (const m of data.masters) {
|
|
779
|
+
const why = m.matched.length
|
|
780
|
+
? `命中 ${m.matched.slice(0, 5).join("、")}`
|
|
781
|
+
: data.note || "主题配对";
|
|
782
|
+
console.log(` ${m.command} [${m.tradition}] ${why}`);
|
|
783
|
+
}
|
|
784
|
+
if (data.resolvedBy === "default_pairing") {
|
|
785
|
+
console.log(`\n 没有明确命中,给的是通用入门配对。`);
|
|
786
|
+
}
|
|
787
|
+
console.log(
|
|
788
|
+
`\n(想看多位祖师并列 → /compare-masters;想看对辩 → /master-debate;` +
|
|
789
|
+
`想要学修路径 → /master-curriculum)\n`
|
|
790
|
+
);
|
|
791
|
+
return 0;
|
|
792
|
+
}
|
|
793
|
+
|
|
339
794
|
function showHelp() {
|
|
340
795
|
console.log(`
|
|
341
796
|
master-skill v${pkgVersion()} — Buddhist Master AI Skills installer
|
|
342
797
|
|
|
343
798
|
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
|
|
799
|
+
master-skill install <name...> Install skills to ~/.claude/skills/
|
|
800
|
+
master-skill install --all Install all ${CATALOG.skills.length} available skills
|
|
801
|
+
master-skill update --all Reinstall all skills, clearing stale files
|
|
802
|
+
master-skill list List available skills
|
|
803
|
+
master-skill list --json Print available skills as JSON
|
|
349
804
|
master-skill inspect <name> Show source/runtime metadata for one master
|
|
350
805
|
master-skill inspect <name> --json
|
|
806
|
+
master-skill recommend "<问题>" Suggest which master or teaching mode to use
|
|
807
|
+
master-skill recommend "<问题>" --json
|
|
351
808
|
master-skill doctor Check local install and runtime paths
|
|
352
809
|
master-skill doctor --json Print diagnostics as JSON
|
|
353
|
-
master-skill uninstall <name...> Remove installed
|
|
810
|
+
master-skill uninstall <name...> Remove installed skills
|
|
354
811
|
master-skill --version Print version
|
|
355
812
|
master-skill --help Show this help
|
|
356
813
|
|
|
357
|
-
|
|
358
|
-
|
|
814
|
+
Persona names accept both short (zhiyi) and full (master-zhiyi) forms.
|
|
815
|
+
Teaching modes and create-master use their public catalog names.
|
|
359
816
|
|
|
360
817
|
Examples:
|
|
361
818
|
npx master-skill install zhiyi fazang
|
|
362
819
|
npx master-skill install master-milarepa master-tsongkhapa
|
|
820
|
+
npx master-skill install compare-masters
|
|
821
|
+
npx master-skill install create-master
|
|
363
822
|
npx master-skill install --all
|
|
364
823
|
npx master-skill update --all
|
|
365
824
|
npx master-skill list
|
|
366
825
|
npx master-skill inspect huineng
|
|
826
|
+
npx master-skill recommend "念佛怎么念才算老实"
|
|
827
|
+
npx master-skill recommend "禅宗从哪开始学"
|
|
367
828
|
npx master-skill doctor
|
|
368
829
|
npx master-skill uninstall zhiyi
|
|
369
830
|
`);
|
|
@@ -371,48 +832,54 @@ Examples:
|
|
|
371
832
|
|
|
372
833
|
// --- main ---
|
|
373
834
|
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
835
|
+
if (CATALOG) {
|
|
836
|
+
const args = process.argv.slice(2);
|
|
837
|
+
const json = args.includes("--json");
|
|
838
|
+
const positionalArgs = args.filter((arg) => arg !== "--json");
|
|
839
|
+
const cmd = positionalArgs[0];
|
|
840
|
+
|
|
841
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
842
|
+
showHelp();
|
|
843
|
+
} else if (cmd === "--version" || cmd === "-v") {
|
|
844
|
+
console.log(pkgVersion());
|
|
845
|
+
} else if (cmd === "list") {
|
|
846
|
+
cmdList({ json });
|
|
847
|
+
} else if (cmd === "doctor") {
|
|
848
|
+
if (cmdDoctor({ json }) > 0) process.exitCode = 1;
|
|
849
|
+
} else if (cmd === "inspect") {
|
|
850
|
+
if (cmdInspect(positionalArgs[1], { json }) > 0) process.exitCode = 1;
|
|
851
|
+
} else if (cmd === "recommend") {
|
|
852
|
+
// Join the rest so an unquoted multi-word query still works.
|
|
853
|
+
const query = positionalArgs.slice(1).join(" ");
|
|
854
|
+
if (cmdRecommend(query, { json }) > 0) process.exitCode = 1;
|
|
855
|
+
} else if (cmd === "install") {
|
|
856
|
+
const rest = positionalArgs.slice(1);
|
|
857
|
+
if (rest.includes("--all")) {
|
|
858
|
+
if (cmdInstallAll("Installing") > 0) process.exitCode = 1;
|
|
859
|
+
} else if (rest.length === 0) {
|
|
860
|
+
console.log("Usage: master-skill install <name...> | --all");
|
|
861
|
+
process.exitCode = 1;
|
|
862
|
+
} else {
|
|
863
|
+
if (cmdInstall(rest) > 0) process.exitCode = 1;
|
|
864
|
+
}
|
|
865
|
+
} else if (cmd === "update") {
|
|
866
|
+
const rest = positionalArgs.slice(1);
|
|
867
|
+
if (rest.length === 1 && rest[0] === "--all") {
|
|
868
|
+
if (cmdInstallAll("Updating") > 0) process.exitCode = 1;
|
|
869
|
+
} else {
|
|
870
|
+
console.log("Usage: master-skill update --all");
|
|
871
|
+
process.exitCode = 1;
|
|
872
|
+
}
|
|
873
|
+
} else if (cmd === "uninstall") {
|
|
874
|
+
const rest = positionalArgs.slice(1);
|
|
875
|
+
if (rest.length === 0) {
|
|
876
|
+
console.log("Usage: master-skill uninstall <name...>");
|
|
877
|
+
process.exitCode = 1;
|
|
878
|
+
} else {
|
|
879
|
+
if (cmdUninstall(rest) > 0) process.exitCode = 1;
|
|
880
|
+
}
|
|
403
881
|
} else {
|
|
404
|
-
console.log(
|
|
882
|
+
console.log(`Unknown command: ${cmd}\nRun master-skill --help for usage.`);
|
|
405
883
|
process.exitCode = 1;
|
|
406
884
|
}
|
|
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
885
|
}
|