@lifeaitools/rdc-skills 0.24.38 → 0.24.41
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/settings.json +15 -15
- package/.claude-plugin/marketplace.json +21 -21
- package/.claude-plugin/plugin.json +1371 -1371
- package/.github/workflows/publish.yml +34 -34
- package/.github/workflows/self-test.yml +58 -58
- package/CHANGELOG.md +310 -310
- package/LICENSE +21 -21
- package/MANIFEST.md +221 -221
- package/README.md +376 -375
- package/README.sandbox.md +3 -3
- package/RELEASE.md +42 -0
- package/assets/watcher/viewer.html +164 -164
- package/bin/rdc-skills-mcp.mjs +316 -316
- package/commands/build.md +183 -183
- package/commands/collab.md +180 -180
- package/commands/deploy.md +152 -152
- package/commands/design.md +31 -31
- package/commands/edit.md +28 -28
- package/commands/fixit.md +124 -124
- package/commands/handoff.md +173 -173
- package/commands/help.md +95 -95
- package/commands/overnight.md +220 -220
- package/commands/plan.md +158 -158
- package/commands/preplan.md +131 -131
- package/commands/prototype.md +145 -145
- package/commands/release.md +49 -49
- package/commands/report.md +99 -99
- package/commands/review.md +120 -120
- package/commands/self-test.md +113 -113
- package/commands/status.md +86 -86
- package/commands/watch.md +98 -98
- package/commands/workitems.md +137 -137
- package/git-sha.json +1 -1
- package/guides/agent-bootstrap.md +295 -295
- package/guides/agents/backend.md +104 -104
- package/guides/agents/content.md +94 -94
- package/guides/agents/cs2.md +56 -56
- package/guides/agents/data.md +87 -87
- package/guides/agents/design.md +77 -77
- package/guides/agents/frontend.md +92 -92
- package/guides/agents/infrastructure.md +81 -81
- package/guides/agents/setup.md +281 -281
- package/guides/agents/verify.md +151 -151
- package/guides/agents/viz.md +106 -106
- package/guides/backend.md +146 -146
- package/guides/content.md +147 -147
- package/guides/cs2.md +190 -190
- package/guides/data.md +123 -123
- package/guides/design.md +116 -116
- package/guides/engineering-behavior.md +43 -43
- package/guides/escalation-protocol.md +125 -125
- package/guides/frontend.md +151 -151
- package/guides/history-md-spec.md +297 -297
- package/guides/infrastructure.md +179 -179
- package/guides/lessons-learned-spec.md +151 -145
- package/guides/output-contract.md +108 -108
- package/guides/publish-md-spec.md +289 -289
- package/guides/rdc-skills-startup.md +30 -30
- package/guides/verify.md +11 -11
- package/hooks/check-cwd.js +31 -31
- package/hooks/check-rdc-environment.js +164 -164
- package/hooks/check-services.js +6 -6
- package/hooks/check-stale-work-items.js +19 -19
- package/hooks/foreground-process-gate.js +128 -128
- package/hooks/gate-watchdog-selfcheck.js +257 -257
- package/hooks/hook-logger.js +25 -25
- package/hooks/lib/run-evidence-gate.mjs +241 -241
- package/hooks/no-stop-open-epics.js +127 -127
- package/hooks/post-tool-batch-gate.js +203 -203
- package/hooks/post-work-check.js +21 -21
- package/hooks/postcompact-log.js +13 -13
- package/hooks/precompact-log.js +13 -13
- package/hooks/rate-limit-retry.js +46 -46
- package/hooks/rdc-invocation-marker.js +157 -157
- package/hooks/rdc-output-contract-gate.js +94 -94
- package/hooks/require-work-item-on-commit.js +294 -294
- package/hooks/restart-brief.js +19 -19
- package/hooks/run-hidden-hook.ps1 +47 -47
- package/hooks/task-completed-gate.js +274 -274
- package/hooks/work-item-exit-gate.js +944 -944
- package/lib/catalog.mjs +236 -236
- package/lib/cloud-rewrite.mjs +155 -155
- package/package.json +57 -56
- package/rules/work-items-rpc.md +520 -520
- package/scaffold/templates/HISTORY.md.template +39 -39
- package/scaffold/templates/PUBLISH.md.template +21 -21
- package/scaffold/templates/brochure-studio-default.html +70 -70
- package/scripts/acceptance.mjs +502 -502
- package/scripts/fixtures/guides/bad-guide.md +15 -15
- package/scripts/fixtures/guides-clean/good-guide.md +16 -16
- package/scripts/install-rdc-skills.js +1289 -1289
- package/scripts/install.ps1 +202 -202
- package/scripts/install.sh +132 -132
- package/scripts/lib/assertions.mjs +287 -287
- package/scripts/lib/manifest-schema.mjs +754 -754
- package/scripts/lib/runner.mjs +465 -465
- package/scripts/lib/sandbox.mjs +435 -435
- package/scripts/prepack.mjs +32 -32
- package/scripts/rdc-brochure.mjs +482 -464
- package/scripts/rdc-design-cli.mjs +134 -134
- package/scripts/rebuild-mcp.mjs +107 -107
- package/scripts/self-test.mjs +1460 -1460
- package/scripts/stamp-git-sha.mjs +29 -29
- package/scripts/test-guide-validator.mjs +196 -196
- package/scripts/test-rdc-hooks.mjs +145 -145
- package/scripts/uninstall.ps1 +77 -77
- package/scripts/uninstall.sh +69 -69
- package/scripts/update.ps1 +43 -43
- package/scripts/update.sh +43 -43
- package/scripts/validate-place-histories.js +461 -461
- package/scripts/validate-publish-manifests.js +424 -424
- package/scripts/watch-init.mjs +100 -100
- package/skills/brochure/SKILL.md +107 -107
- package/skills/build/SKILL.md +563 -563
- package/skills/channel-formatter/SKILL.md +533 -533
- package/skills/co-develop/SKILL.md +196 -196
- package/skills/collab/SKILL.md +239 -239
- package/skills/convert/SKILL.md +140 -140
- package/skills/deploy/SKILL.md +541 -541
- package/skills/design/SKILL.md +211 -211
- package/skills/design/reference/ownership.md +16 -16
- package/skills/design/reference/rampa.md +92 -92
- package/skills/design/reference/studio-model.md +153 -153
- package/skills/edit/SKILL.md +98 -98
- package/skills/fixit/SKILL.md +165 -165
- package/skills/fs-mcp/SKILL.md +148 -148
- package/skills/handoff/SKILL.md +236 -200
- package/skills/help/SKILL.md +143 -143
- package/skills/housekeeping/SKILL.md +219 -160
- package/skills/lifeai-brochure-author/SKILL.md +340 -340
- package/skills/overnight/SKILL.md +251 -251
- package/skills/plan/SKILL.md +345 -345
- package/skills/preplan/SKILL.md +90 -90
- package/skills/prototype/SKILL.md +150 -150
- package/skills/rdc-brochurify/SKILL.md +245 -245
- package/skills/rdc-extract-verifier-rules/SKILL.md +191 -191
- package/skills/release/SKILL.md +140 -140
- package/skills/report/SKILL.md +100 -100
- package/skills/review/SKILL.md +152 -152
- package/skills/rpms-filemap/SKILL.cloud.md +111 -111
- package/skills/rpms-filemap/SKILL.md +111 -111
- package/skills/self-test/SKILL.md +132 -132
- package/skills/status/SKILL.md +99 -99
- package/skills/terminal-config/SKILL.md +62 -62
- package/skills/tests/MATRIX.md +54 -54
- package/skills/tests/README.md +47 -47
- package/skills/tests/rdc-brochure.test.json +34 -34
- package/skills/tests/rdc-build.test.json +36 -36
- package/skills/tests/rdc-channel-formatter.test.json +45 -45
- package/skills/tests/rdc-co-develop.test.json +29 -29
- package/skills/tests/rdc-collab.test.json +29 -29
- package/skills/tests/rdc-convert.test.json +35 -35
- package/skills/tests/rdc-deploy.test.json +30 -30
- package/skills/tests/rdc-design.test.json +27 -27
- package/skills/tests/rdc-edit.test.json +29 -29
- package/skills/tests/rdc-fixit.test.json +36 -36
- package/skills/tests/rdc-fs-mcp.test.json +36 -36
- package/skills/tests/rdc-handoff.test.json +28 -28
- package/skills/tests/rdc-help.test.json +29 -29
- package/skills/tests/rdc-housekeeping.test.json +32 -28
- package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
- package/skills/tests/rdc-overnight.test.json +37 -37
- package/skills/tests/rdc-plan.test.json +27 -27
- package/skills/tests/rdc-preplan.test.json +31 -31
- package/skills/tests/rdc-prototype.test.json +28 -28
- package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
- package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
- package/skills/tests/rdc-release.test.json +29 -29
- package/skills/tests/rdc-report.test.json +28 -28
- package/skills/tests/rdc-review.test.json +29 -29
- package/skills/tests/rdc-rpms-filemap.test.json +28 -28
- package/skills/tests/rdc-self-test.test.json +24 -24
- package/skills/tests/rdc-status.test.json +29 -29
- package/skills/tests/rdc-terminal-config.test.json +29 -29
- package/skills/tests/rdc-watch.test.json +24 -24
- package/skills/tests/rdc-workitems.test.json +27 -27
- package/skills/watch/SKILL.md +97 -97
- package/skills/workitems/SKILL.md +151 -151
- package/tests/acceptance.test.mjs +59 -59
- package/tests/channel-formatter.contract.test.mjs +251 -251
- package/tests/curl-surface.test.mjs +289 -289
- package/tests/harness-gates.test.mjs +325 -325
- package/tests/help-surface.test.mjs +61 -61
- package/tests/housekeeping-lessons-triage.test.mjs +49 -0
- package/tests/install-rdc-skills.test.mjs +49 -49
- package/tests/lessons-pipeline-contract.test.mjs +26 -0
- package/tests/manifest-contract-fields.test.mjs +78 -78
- package/tests/mcp.test.mjs +271 -271
- package/tests/rdc-brochure.test.mjs +125 -0
- package/tests/release-contract.test.mjs +16 -0
- package/tests/require-work-item-on-commit.test.mjs +162 -162
- package/tests/run-evidence-gate.test.mjs +82 -82
- package/tests/skill-test-matrix.test.mjs +66 -66
- package/tests/validate-skills.js +27 -27
- package/tests/work-item-exit-gate-l2.test.mjs +368 -368
- package/tests/work-item-exit-gate-l3.test.mjs +197 -197
package/lib/catalog.mjs
CHANGED
|
@@ -1,236 +1,236 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/catalog.mjs — rdc-skills catalog loader for the local MCP server.
|
|
3
|
-
*
|
|
4
|
-
* Source of truth: `.claude-plugin/plugin.json` → `skills_meta` (an object keyed
|
|
5
|
-
* by skill name). Each entry provides { name, slash, category, usage, args,
|
|
6
|
-
* requires, triggers, ... }. We enrich each entry with the `description` from
|
|
7
|
-
* the skill's own `skills/<name>/SKILL.md` frontmatter (the human summary).
|
|
8
|
-
*
|
|
9
|
-
* Loading is LIVE: the catalog is re-read from disk when older than CACHE_TTL_MS
|
|
10
|
-
* so a skill edit (frontmatter or body) is picked up without a server restart.
|
|
11
|
-
* Published packages must have one `skills_meta` entry for every skill
|
|
12
|
-
* directory. The frontmatter-only path below is a local-dev resilience fallback
|
|
13
|
-
* so an incomplete checkout does not silently hide a skill while it is being
|
|
14
|
-
* repaired; tests enforce that released packages do not rely on it.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import fs from 'node:fs';
|
|
18
|
-
import path from 'node:path';
|
|
19
|
-
import { fileURLToPath } from 'node:url';
|
|
20
|
-
import YAML from 'yaml';
|
|
21
|
-
|
|
22
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
24
|
-
const PLUGIN_JSON = path.join(REPO_ROOT, '.claude-plugin', 'plugin.json');
|
|
25
|
-
const SKILLS_DIR = path.join(REPO_ROOT, 'skills');
|
|
26
|
-
|
|
27
|
-
const CACHE_TTL_MS = 5000; // short TTL: live-from-disk feel, cheap re-reads
|
|
28
|
-
|
|
29
|
-
let _cache = null;
|
|
30
|
-
let _cacheAt = 0;
|
|
31
|
-
|
|
32
|
-
/** Read + parse the YAML frontmatter block of a SKILL.md. Returns {} on miss. */
|
|
33
|
-
function readFrontmatter(skillMdPath) {
|
|
34
|
-
try {
|
|
35
|
-
const raw = fs.readFileSync(skillMdPath, 'utf8').replace(/\r\n/g, '\n');
|
|
36
|
-
const m = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
37
|
-
if (!m) return {};
|
|
38
|
-
const parsed = YAML.parse(m[1]);
|
|
39
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
40
|
-
} catch {
|
|
41
|
-
return {};
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Map a raw skills_meta entry + SKILL.md frontmatter into a compact catalog row. */
|
|
46
|
-
function toCatalogEntry(name, meta, frontmatter) {
|
|
47
|
-
const fm = frontmatter || {};
|
|
48
|
-
const description = fm.description || meta.description || '';
|
|
49
|
-
// triggers/usage/slash/etc. should live in skills_meta for published packages.
|
|
50
|
-
// Frontmatter fallback keeps incomplete local checkouts searchable during repair.
|
|
51
|
-
const metaTriggers = Array.isArray(meta.triggers) && meta.triggers.length ? meta.triggers : null;
|
|
52
|
-
const fmTriggers = Array.isArray(fm.triggers) ? fm.triggers : [];
|
|
53
|
-
const slash = meta.slash || fm.slash || `rdc:${name}`;
|
|
54
|
-
const aliases = [name, slash];
|
|
55
|
-
if (slash.startsWith('rdc:')) aliases.push(`/${slash}`);
|
|
56
|
-
return {
|
|
57
|
-
name,
|
|
58
|
-
slash,
|
|
59
|
-
aliases,
|
|
60
|
-
category: meta.category || fm.category || 'tooling',
|
|
61
|
-
summary: description,
|
|
62
|
-
when_to_use: metaTriggers || fmTriggers,
|
|
63
|
-
// usage falls back to the slash form so every skill carries an invocation hint.
|
|
64
|
-
usage: meta.usage || fm.usage || slash,
|
|
65
|
-
args: meta.args || fm.args || { positional: [], flags: [] },
|
|
66
|
-
requires: Array.isArray(meta.requires) ? meta.requires
|
|
67
|
-
: Array.isArray(fm.requires) ? fm.requires : [],
|
|
68
|
-
produces: Array.isArray(meta.produces) ? meta.produces
|
|
69
|
-
: Array.isArray(fm.produces) ? fm.produces : [],
|
|
70
|
-
follows: Array.isArray(meta.follows) ? meta.follows
|
|
71
|
-
: Array.isArray(fm.follows) ? fm.follows : [],
|
|
72
|
-
leads_to: Array.isArray(meta.leads_to) ? meta.leads_to
|
|
73
|
-
: Array.isArray(fm.leads_to) ? fm.leads_to : [],
|
|
74
|
-
default_model: meta.default_model || fm.default_model || 'inherit',
|
|
75
|
-
sandbox_aware: Boolean(meta.sandbox_aware ?? fm.sandbox_aware ?? false),
|
|
76
|
-
output_contract: meta.output_contract || fm.output_contract || null,
|
|
77
|
-
enabled_default: Boolean(meta.enabled_default ?? fm.enabled_default ?? true),
|
|
78
|
-
codeflow_required: Boolean(meta.codeflow_required ?? fm.codeflow_required ?? false),
|
|
79
|
-
variants: ['cli', 'cloud'],
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Re-read plugin.json + skill frontmatter from disk and rebuild the catalog. */
|
|
84
|
-
function buildCatalog() {
|
|
85
|
-
let plugin = {};
|
|
86
|
-
try {
|
|
87
|
-
plugin = JSON.parse(fs.readFileSync(PLUGIN_JSON, 'utf8'));
|
|
88
|
-
} catch {
|
|
89
|
-
plugin = {};
|
|
90
|
-
}
|
|
91
|
-
const skillsMeta = plugin.skills_meta && typeof plugin.skills_meta === 'object' ? plugin.skills_meta : {};
|
|
92
|
-
|
|
93
|
-
const byName = new Map();
|
|
94
|
-
|
|
95
|
-
// 1. Every skills_meta entry → enrich with its SKILL.md frontmatter.
|
|
96
|
-
for (const [name, meta] of Object.entries(skillsMeta)) {
|
|
97
|
-
const skillMd = path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
98
|
-
const fm = fs.existsSync(skillMd) ? readFrontmatter(skillMd) : {};
|
|
99
|
-
byName.set(name, toCatalogEntry(name, meta || {}, fm));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 2. Local-dev fallback: a skill dir with SKILL.md but no meta entry.
|
|
103
|
-
if (fs.existsSync(SKILLS_DIR)) {
|
|
104
|
-
for (const entry of fs.readdirSync(SKILLS_DIR, { withFileTypes: true })) {
|
|
105
|
-
if (!entry.isDirectory() || byName.has(entry.name)) continue;
|
|
106
|
-
const skillMd = path.join(SKILLS_DIR, entry.name, 'SKILL.md');
|
|
107
|
-
if (!fs.existsSync(skillMd)) continue;
|
|
108
|
-
const fm = readFrontmatter(skillMd);
|
|
109
|
-
byName.set(entry.name, toCatalogEntry(entry.name, {}, fm));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Return the cached catalog, rebuilding if the TTL has elapsed. */
|
|
117
|
-
function getCatalog() {
|
|
118
|
-
const now = Date.now();
|
|
119
|
-
if (!_cache || now - _cacheAt > CACHE_TTL_MS) {
|
|
120
|
-
_cache = buildCatalog();
|
|
121
|
-
_cacheAt = now;
|
|
122
|
-
}
|
|
123
|
-
return _cache;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Compact catalog for `rdc_skill_list`. */
|
|
127
|
-
export function listSkills() {
|
|
128
|
-
return getCatalog();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Full catalog row for one skill, or null if unknown. */
|
|
132
|
-
export function getSkill(name) {
|
|
133
|
-
const resolved = resolveSkillName(name);
|
|
134
|
-
return resolved ? getCatalog().find((s) => s.name === resolved) || null : null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** List of valid skill names (for error messages). */
|
|
138
|
-
export function skillNames() {
|
|
139
|
-
return getCatalog().map((s) => s.name);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Return the canonical skill directory name for a bare name, slash, or usage token. */
|
|
143
|
-
export function resolveSkillName(input) {
|
|
144
|
-
const raw = String(input || '').trim();
|
|
145
|
-
if (!raw) return null;
|
|
146
|
-
const token = raw.split(/\s+/)[0];
|
|
147
|
-
const catalog = getCatalog();
|
|
148
|
-
const direct = catalog.find((s) => s.name === token || s.slash === token);
|
|
149
|
-
if (direct) return direct.name;
|
|
150
|
-
if (token.startsWith('/')) {
|
|
151
|
-
const withoutSlash = token.slice(1);
|
|
152
|
-
const slashMatch = catalog.find((s) => s.slash === withoutSlash);
|
|
153
|
-
if (slashMatch) return slashMatch.name;
|
|
154
|
-
}
|
|
155
|
-
if (token.startsWith('rdc:')) {
|
|
156
|
-
const bare = token.slice(4);
|
|
157
|
-
const bareMatch = catalog.find((s) => s.name === bare || s.name === `rdc-${bare}`);
|
|
158
|
-
if (bareMatch) return bareMatch.name;
|
|
159
|
-
}
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Read the raw SKILL.md body (frontmatter stripped) for a skill, live from disk.
|
|
165
|
-
* Returns null if the file does not exist.
|
|
166
|
-
*/
|
|
167
|
-
export function getSkillBody(name) {
|
|
168
|
-
const resolved = resolveSkillName(name);
|
|
169
|
-
if (!resolved) return null;
|
|
170
|
-
const skillMd = path.join(SKILLS_DIR, resolved, 'SKILL.md');
|
|
171
|
-
if (!fs.existsSync(skillMd)) return null;
|
|
172
|
-
const raw = fs.readFileSync(skillMd, 'utf8').replace(/\r\n/g, '\n');
|
|
173
|
-
// Strip the leading frontmatter block if present.
|
|
174
|
-
const m = raw.match(/^---\n[\s\S]*?\n---\n?/);
|
|
175
|
-
return m ? raw.slice(m[0].length).replace(/^\n+/, '') : raw;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** Absolute path to a hand-tuned cloud override, or null if absent. */
|
|
179
|
-
export function cloudOverridePath(name) {
|
|
180
|
-
const resolved = resolveSkillName(name);
|
|
181
|
-
if (!resolved) return null;
|
|
182
|
-
const p = path.join(SKILLS_DIR, resolved, 'SKILL.cloud.md');
|
|
183
|
-
return fs.existsSync(p) ? p : null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** Read the hand-tuned cloud override body (frontmatter stripped), or null. */
|
|
187
|
-
export function getCloudOverride(name) {
|
|
188
|
-
const p = cloudOverridePath(name);
|
|
189
|
-
if (!p) return null;
|
|
190
|
-
const raw = fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n');
|
|
191
|
-
const m = raw.match(/^---\n[\s\S]*?\n---\n?/);
|
|
192
|
-
return m ? raw.slice(m[0].length).replace(/^\n+/, '') : raw;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Fuzzy/substring search over name + slash + summary + triggers.
|
|
197
|
-
* Returns ranked rows with enough metadata for raw MCP/curl callers to choose
|
|
198
|
-
* and fetch a skill without a second catalog lookup. score: higher = better.
|
|
199
|
-
*/
|
|
200
|
-
export function searchSkills(query) {
|
|
201
|
-
const q = String(query || '').trim().toLowerCase();
|
|
202
|
-
if (!q) return [];
|
|
203
|
-
const terms = q.split(/\s+/).filter(Boolean);
|
|
204
|
-
const results = [];
|
|
205
|
-
for (const s of getCatalog()) {
|
|
206
|
-
const hay = {
|
|
207
|
-
name: s.name.toLowerCase(),
|
|
208
|
-
slash: (s.slash || '').toLowerCase(),
|
|
209
|
-
summary: (s.summary || '').toLowerCase(),
|
|
210
|
-
triggers: (s.when_to_use || []).join(' ').toLowerCase(),
|
|
211
|
-
};
|
|
212
|
-
let score = 0;
|
|
213
|
-
for (const term of terms) {
|
|
214
|
-
if (hay.name === term) score += 100;
|
|
215
|
-
else if (hay.name.includes(term)) score += 40;
|
|
216
|
-
if (hay.slash.includes(term)) score += 20;
|
|
217
|
-
if (hay.triggers.includes(term)) score += 12;
|
|
218
|
-
if (hay.summary.includes(term)) score += 6;
|
|
219
|
-
}
|
|
220
|
-
if (score > 0) {
|
|
221
|
-
results.push({
|
|
222
|
-
name: s.name,
|
|
223
|
-
slash: s.slash,
|
|
224
|
-
aliases: s.aliases,
|
|
225
|
-
category: s.category,
|
|
226
|
-
summary: s.summary,
|
|
227
|
-
usage: s.usage,
|
|
228
|
-
requires: s.requires,
|
|
229
|
-
codeflow_required: s.codeflow_required,
|
|
230
|
-
variants: s.variants,
|
|
231
|
-
score,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return results.sort((a, b) => b.score - a.score);
|
|
236
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* lib/catalog.mjs — rdc-skills catalog loader for the local MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Source of truth: `.claude-plugin/plugin.json` → `skills_meta` (an object keyed
|
|
5
|
+
* by skill name). Each entry provides { name, slash, category, usage, args,
|
|
6
|
+
* requires, triggers, ... }. We enrich each entry with the `description` from
|
|
7
|
+
* the skill's own `skills/<name>/SKILL.md` frontmatter (the human summary).
|
|
8
|
+
*
|
|
9
|
+
* Loading is LIVE: the catalog is re-read from disk when older than CACHE_TTL_MS
|
|
10
|
+
* so a skill edit (frontmatter or body) is picked up without a server restart.
|
|
11
|
+
* Published packages must have one `skills_meta` entry for every skill
|
|
12
|
+
* directory. The frontmatter-only path below is a local-dev resilience fallback
|
|
13
|
+
* so an incomplete checkout does not silently hide a skill while it is being
|
|
14
|
+
* repaired; tests enforce that released packages do not rely on it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import YAML from 'yaml';
|
|
21
|
+
|
|
22
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
24
|
+
const PLUGIN_JSON = path.join(REPO_ROOT, '.claude-plugin', 'plugin.json');
|
|
25
|
+
const SKILLS_DIR = path.join(REPO_ROOT, 'skills');
|
|
26
|
+
|
|
27
|
+
const CACHE_TTL_MS = 5000; // short TTL: live-from-disk feel, cheap re-reads
|
|
28
|
+
|
|
29
|
+
let _cache = null;
|
|
30
|
+
let _cacheAt = 0;
|
|
31
|
+
|
|
32
|
+
/** Read + parse the YAML frontmatter block of a SKILL.md. Returns {} on miss. */
|
|
33
|
+
function readFrontmatter(skillMdPath) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = fs.readFileSync(skillMdPath, 'utf8').replace(/\r\n/g, '\n');
|
|
36
|
+
const m = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
37
|
+
if (!m) return {};
|
|
38
|
+
const parsed = YAML.parse(m[1]);
|
|
39
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
40
|
+
} catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Map a raw skills_meta entry + SKILL.md frontmatter into a compact catalog row. */
|
|
46
|
+
function toCatalogEntry(name, meta, frontmatter) {
|
|
47
|
+
const fm = frontmatter || {};
|
|
48
|
+
const description = fm.description || meta.description || '';
|
|
49
|
+
// triggers/usage/slash/etc. should live in skills_meta for published packages.
|
|
50
|
+
// Frontmatter fallback keeps incomplete local checkouts searchable during repair.
|
|
51
|
+
const metaTriggers = Array.isArray(meta.triggers) && meta.triggers.length ? meta.triggers : null;
|
|
52
|
+
const fmTriggers = Array.isArray(fm.triggers) ? fm.triggers : [];
|
|
53
|
+
const slash = meta.slash || fm.slash || `rdc:${name}`;
|
|
54
|
+
const aliases = [name, slash];
|
|
55
|
+
if (slash.startsWith('rdc:')) aliases.push(`/${slash}`);
|
|
56
|
+
return {
|
|
57
|
+
name,
|
|
58
|
+
slash,
|
|
59
|
+
aliases,
|
|
60
|
+
category: meta.category || fm.category || 'tooling',
|
|
61
|
+
summary: description,
|
|
62
|
+
when_to_use: metaTriggers || fmTriggers,
|
|
63
|
+
// usage falls back to the slash form so every skill carries an invocation hint.
|
|
64
|
+
usage: meta.usage || fm.usage || slash,
|
|
65
|
+
args: meta.args || fm.args || { positional: [], flags: [] },
|
|
66
|
+
requires: Array.isArray(meta.requires) ? meta.requires
|
|
67
|
+
: Array.isArray(fm.requires) ? fm.requires : [],
|
|
68
|
+
produces: Array.isArray(meta.produces) ? meta.produces
|
|
69
|
+
: Array.isArray(fm.produces) ? fm.produces : [],
|
|
70
|
+
follows: Array.isArray(meta.follows) ? meta.follows
|
|
71
|
+
: Array.isArray(fm.follows) ? fm.follows : [],
|
|
72
|
+
leads_to: Array.isArray(meta.leads_to) ? meta.leads_to
|
|
73
|
+
: Array.isArray(fm.leads_to) ? fm.leads_to : [],
|
|
74
|
+
default_model: meta.default_model || fm.default_model || 'inherit',
|
|
75
|
+
sandbox_aware: Boolean(meta.sandbox_aware ?? fm.sandbox_aware ?? false),
|
|
76
|
+
output_contract: meta.output_contract || fm.output_contract || null,
|
|
77
|
+
enabled_default: Boolean(meta.enabled_default ?? fm.enabled_default ?? true),
|
|
78
|
+
codeflow_required: Boolean(meta.codeflow_required ?? fm.codeflow_required ?? false),
|
|
79
|
+
variants: ['cli', 'cloud'],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Re-read plugin.json + skill frontmatter from disk and rebuild the catalog. */
|
|
84
|
+
function buildCatalog() {
|
|
85
|
+
let plugin = {};
|
|
86
|
+
try {
|
|
87
|
+
plugin = JSON.parse(fs.readFileSync(PLUGIN_JSON, 'utf8'));
|
|
88
|
+
} catch {
|
|
89
|
+
plugin = {};
|
|
90
|
+
}
|
|
91
|
+
const skillsMeta = plugin.skills_meta && typeof plugin.skills_meta === 'object' ? plugin.skills_meta : {};
|
|
92
|
+
|
|
93
|
+
const byName = new Map();
|
|
94
|
+
|
|
95
|
+
// 1. Every skills_meta entry → enrich with its SKILL.md frontmatter.
|
|
96
|
+
for (const [name, meta] of Object.entries(skillsMeta)) {
|
|
97
|
+
const skillMd = path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
98
|
+
const fm = fs.existsSync(skillMd) ? readFrontmatter(skillMd) : {};
|
|
99
|
+
byName.set(name, toCatalogEntry(name, meta || {}, fm));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 2. Local-dev fallback: a skill dir with SKILL.md but no meta entry.
|
|
103
|
+
if (fs.existsSync(SKILLS_DIR)) {
|
|
104
|
+
for (const entry of fs.readdirSync(SKILLS_DIR, { withFileTypes: true })) {
|
|
105
|
+
if (!entry.isDirectory() || byName.has(entry.name)) continue;
|
|
106
|
+
const skillMd = path.join(SKILLS_DIR, entry.name, 'SKILL.md');
|
|
107
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
108
|
+
const fm = readFrontmatter(skillMd);
|
|
109
|
+
byName.set(entry.name, toCatalogEntry(entry.name, {}, fm));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Return the cached catalog, rebuilding if the TTL has elapsed. */
|
|
117
|
+
function getCatalog() {
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
if (!_cache || now - _cacheAt > CACHE_TTL_MS) {
|
|
120
|
+
_cache = buildCatalog();
|
|
121
|
+
_cacheAt = now;
|
|
122
|
+
}
|
|
123
|
+
return _cache;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Compact catalog for `rdc_skill_list`. */
|
|
127
|
+
export function listSkills() {
|
|
128
|
+
return getCatalog();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Full catalog row for one skill, or null if unknown. */
|
|
132
|
+
export function getSkill(name) {
|
|
133
|
+
const resolved = resolveSkillName(name);
|
|
134
|
+
return resolved ? getCatalog().find((s) => s.name === resolved) || null : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** List of valid skill names (for error messages). */
|
|
138
|
+
export function skillNames() {
|
|
139
|
+
return getCatalog().map((s) => s.name);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Return the canonical skill directory name for a bare name, slash, or usage token. */
|
|
143
|
+
export function resolveSkillName(input) {
|
|
144
|
+
const raw = String(input || '').trim();
|
|
145
|
+
if (!raw) return null;
|
|
146
|
+
const token = raw.split(/\s+/)[0];
|
|
147
|
+
const catalog = getCatalog();
|
|
148
|
+
const direct = catalog.find((s) => s.name === token || s.slash === token);
|
|
149
|
+
if (direct) return direct.name;
|
|
150
|
+
if (token.startsWith('/')) {
|
|
151
|
+
const withoutSlash = token.slice(1);
|
|
152
|
+
const slashMatch = catalog.find((s) => s.slash === withoutSlash);
|
|
153
|
+
if (slashMatch) return slashMatch.name;
|
|
154
|
+
}
|
|
155
|
+
if (token.startsWith('rdc:')) {
|
|
156
|
+
const bare = token.slice(4);
|
|
157
|
+
const bareMatch = catalog.find((s) => s.name === bare || s.name === `rdc-${bare}`);
|
|
158
|
+
if (bareMatch) return bareMatch.name;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Read the raw SKILL.md body (frontmatter stripped) for a skill, live from disk.
|
|
165
|
+
* Returns null if the file does not exist.
|
|
166
|
+
*/
|
|
167
|
+
export function getSkillBody(name) {
|
|
168
|
+
const resolved = resolveSkillName(name);
|
|
169
|
+
if (!resolved) return null;
|
|
170
|
+
const skillMd = path.join(SKILLS_DIR, resolved, 'SKILL.md');
|
|
171
|
+
if (!fs.existsSync(skillMd)) return null;
|
|
172
|
+
const raw = fs.readFileSync(skillMd, 'utf8').replace(/\r\n/g, '\n');
|
|
173
|
+
// Strip the leading frontmatter block if present.
|
|
174
|
+
const m = raw.match(/^---\n[\s\S]*?\n---\n?/);
|
|
175
|
+
return m ? raw.slice(m[0].length).replace(/^\n+/, '') : raw;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Absolute path to a hand-tuned cloud override, or null if absent. */
|
|
179
|
+
export function cloudOverridePath(name) {
|
|
180
|
+
const resolved = resolveSkillName(name);
|
|
181
|
+
if (!resolved) return null;
|
|
182
|
+
const p = path.join(SKILLS_DIR, resolved, 'SKILL.cloud.md');
|
|
183
|
+
return fs.existsSync(p) ? p : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Read the hand-tuned cloud override body (frontmatter stripped), or null. */
|
|
187
|
+
export function getCloudOverride(name) {
|
|
188
|
+
const p = cloudOverridePath(name);
|
|
189
|
+
if (!p) return null;
|
|
190
|
+
const raw = fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n');
|
|
191
|
+
const m = raw.match(/^---\n[\s\S]*?\n---\n?/);
|
|
192
|
+
return m ? raw.slice(m[0].length).replace(/^\n+/, '') : raw;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Fuzzy/substring search over name + slash + summary + triggers.
|
|
197
|
+
* Returns ranked rows with enough metadata for raw MCP/curl callers to choose
|
|
198
|
+
* and fetch a skill without a second catalog lookup. score: higher = better.
|
|
199
|
+
*/
|
|
200
|
+
export function searchSkills(query) {
|
|
201
|
+
const q = String(query || '').trim().toLowerCase();
|
|
202
|
+
if (!q) return [];
|
|
203
|
+
const terms = q.split(/\s+/).filter(Boolean);
|
|
204
|
+
const results = [];
|
|
205
|
+
for (const s of getCatalog()) {
|
|
206
|
+
const hay = {
|
|
207
|
+
name: s.name.toLowerCase(),
|
|
208
|
+
slash: (s.slash || '').toLowerCase(),
|
|
209
|
+
summary: (s.summary || '').toLowerCase(),
|
|
210
|
+
triggers: (s.when_to_use || []).join(' ').toLowerCase(),
|
|
211
|
+
};
|
|
212
|
+
let score = 0;
|
|
213
|
+
for (const term of terms) {
|
|
214
|
+
if (hay.name === term) score += 100;
|
|
215
|
+
else if (hay.name.includes(term)) score += 40;
|
|
216
|
+
if (hay.slash.includes(term)) score += 20;
|
|
217
|
+
if (hay.triggers.includes(term)) score += 12;
|
|
218
|
+
if (hay.summary.includes(term)) score += 6;
|
|
219
|
+
}
|
|
220
|
+
if (score > 0) {
|
|
221
|
+
results.push({
|
|
222
|
+
name: s.name,
|
|
223
|
+
slash: s.slash,
|
|
224
|
+
aliases: s.aliases,
|
|
225
|
+
category: s.category,
|
|
226
|
+
summary: s.summary,
|
|
227
|
+
usage: s.usage,
|
|
228
|
+
requires: s.requires,
|
|
229
|
+
codeflow_required: s.codeflow_required,
|
|
230
|
+
variants: s.variants,
|
|
231
|
+
score,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return results.sort((a, b) => b.score - a.score);
|
|
236
|
+
}
|