@modootoday/pmcp 0.1.4 → 0.2.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/README.md +3 -2
- package/dist/{chunk-SLTUWTII.js → chunk-2NOO5RSP.js} +2 -2
- package/dist/chunk-3NNRQ5A4.js +698 -0
- package/dist/{chunk-AJNIMNCV.js → chunk-Y32X35YV.js} +10 -4
- package/dist/cli.js +48 -17
- package/dist/index.d.ts +107 -11
- package/dist/index.js +16 -2
- package/dist/stdio.d.ts +1 -1
- package/dist/stdio.js +2 -2
- package/package.json +3 -2
- package/skills/package-skill-catalog/SKILL.md +10 -8
- package/dist/chunk-N3SHKKWG.js +0 -363
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
// src/intent.ts
|
|
2
|
+
function normaliseIntent(intent) {
|
|
3
|
+
return intent.toLowerCase().replaceAll(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/u).filter(Boolean).join(" ");
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/marketplace.ts
|
|
7
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
8
|
+
import { join as join2, resolve } from "path";
|
|
9
|
+
|
|
10
|
+
// src/catalog.ts
|
|
11
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
12
|
+
import { basename, dirname, join, relative } from "path";
|
|
13
|
+
var SKIPPED_DIRS = /* @__PURE__ */ new Set([
|
|
14
|
+
"node_modules",
|
|
15
|
+
".git",
|
|
16
|
+
".cache",
|
|
17
|
+
".turbo",
|
|
18
|
+
"coverage",
|
|
19
|
+
"__tests__",
|
|
20
|
+
"__fixtures__"
|
|
21
|
+
]);
|
|
22
|
+
var MAX_SKILL_DEPTH = 3;
|
|
23
|
+
function readFrontmatter(source) {
|
|
24
|
+
if (!source.startsWith("---")) return {};
|
|
25
|
+
const end = source.indexOf("\n---", 3);
|
|
26
|
+
if (end === -1) return {};
|
|
27
|
+
const fields = {};
|
|
28
|
+
for (const line of source.slice(3, end).split("\n")) {
|
|
29
|
+
const match = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/u.exec(line);
|
|
30
|
+
if (!match) continue;
|
|
31
|
+
const value = (match[2] ?? "").trim();
|
|
32
|
+
if (value === "" || value.startsWith("#")) continue;
|
|
33
|
+
fields[match[1]] = value.replace(/^["'](.*)["']$/u, "$1");
|
|
34
|
+
}
|
|
35
|
+
return fields;
|
|
36
|
+
}
|
|
37
|
+
function readBody(source) {
|
|
38
|
+
if (!source.startsWith("---")) return source;
|
|
39
|
+
const end = source.indexOf("\n---", 3);
|
|
40
|
+
if (end === -1) return source;
|
|
41
|
+
return source.slice(end + 4).replace(/^(?:\r?\n)+/u, "");
|
|
42
|
+
}
|
|
43
|
+
function skillFiles(packageDir) {
|
|
44
|
+
const found = [];
|
|
45
|
+
const stack = [[packageDir, 0]];
|
|
46
|
+
while (stack.length > 0) {
|
|
47
|
+
const next = stack.pop();
|
|
48
|
+
if (next === void 0) break;
|
|
49
|
+
const [dir, depth] = next;
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
53
|
+
} catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (entry.isFile()) {
|
|
58
|
+
if (entry.name === "SKILL.md") found.push(join(dir, entry.name));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!entry.isDirectory()) continue;
|
|
62
|
+
if (SKIPPED_DIRS.has(entry.name)) continue;
|
|
63
|
+
if (depth + 1 <= MAX_SKILL_DEPTH)
|
|
64
|
+
stack.push([join(dir, entry.name), depth + 1]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return found.sort();
|
|
68
|
+
}
|
|
69
|
+
function packageDirs(root) {
|
|
70
|
+
if (!existsSync(root)) return [];
|
|
71
|
+
const found = [];
|
|
72
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
73
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
74
|
+
if (entry.name === ".bin" || entry.name === ".cache") continue;
|
|
75
|
+
const path = join(root, entry.name);
|
|
76
|
+
if (entry.name.startsWith("@")) {
|
|
77
|
+
if (!existsSync(path)) continue;
|
|
78
|
+
for (const scoped of readdirSync(path, { withFileTypes: true })) {
|
|
79
|
+
if (scoped.isDirectory() || scoped.isSymbolicLink()) {
|
|
80
|
+
found.push(join(path, scoped.name));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
found.push(path);
|
|
86
|
+
}
|
|
87
|
+
return found;
|
|
88
|
+
}
|
|
89
|
+
function readCatalog(options) {
|
|
90
|
+
const scopes = options.scopes ?? [];
|
|
91
|
+
const found = [];
|
|
92
|
+
const seen = /* @__PURE__ */ new Set();
|
|
93
|
+
for (const root of options.roots) {
|
|
94
|
+
for (const packageDir of packageDirs(root)) {
|
|
95
|
+
const manifestPath = join(packageDir, "package.json");
|
|
96
|
+
if (!existsSync(manifestPath)) continue;
|
|
97
|
+
let packageName;
|
|
98
|
+
try {
|
|
99
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
100
|
+
if (!manifest.name) continue;
|
|
101
|
+
packageName = manifest.name;
|
|
102
|
+
} catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (scopes.length > 0 && !scopes.some((s) => packageName.startsWith(s))) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
for (const path of skillFiles(packageDir)) {
|
|
109
|
+
let source;
|
|
110
|
+
try {
|
|
111
|
+
source = readFileSync(path, "utf8");
|
|
112
|
+
} catch {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const front = readFrontmatter(source);
|
|
116
|
+
const description = front["description"] ?? "";
|
|
117
|
+
if (description === "") continue;
|
|
118
|
+
const dirName = basename(dirname(path));
|
|
119
|
+
const slug = front["name"] ?? (dirName === basename(packageDir) ? packageName : dirName);
|
|
120
|
+
const name = `${packageName}/${slug}`;
|
|
121
|
+
if (seen.has(name)) continue;
|
|
122
|
+
seen.add(name);
|
|
123
|
+
found.push({
|
|
124
|
+
name,
|
|
125
|
+
package: packageName,
|
|
126
|
+
slug,
|
|
127
|
+
description,
|
|
128
|
+
path
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const dir of options.marketplaces ?? []) {
|
|
134
|
+
for (const entry of readMarketplace(dir, options.onReject)) {
|
|
135
|
+
if (seen.has(entry.name)) {
|
|
136
|
+
options.onReject?.({
|
|
137
|
+
path: entry.path,
|
|
138
|
+
reason: `duplicate name ${entry.name}`
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
seen.add(entry.name);
|
|
143
|
+
found.push(entry);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return found.sort((a, b) => a.name.localeCompare(b.name));
|
|
147
|
+
}
|
|
148
|
+
function withoutBody({
|
|
149
|
+
path,
|
|
150
|
+
...rest
|
|
151
|
+
}) {
|
|
152
|
+
void path;
|
|
153
|
+
return rest;
|
|
154
|
+
}
|
|
155
|
+
function displayPath(root, entry) {
|
|
156
|
+
return relative(root, entry.path).replaceAll("\\", "/");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/marketplace.ts
|
|
160
|
+
function parseScalar(raw) {
|
|
161
|
+
const value = raw.trim();
|
|
162
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
163
|
+
return value.slice(1, -1).split(",").map((item) => item.trim().replace(/^["'](.*)["']$/u, "$1")).filter(Boolean);
|
|
164
|
+
}
|
|
165
|
+
return value.replace(/^["'](.*)["']$/u, "$1");
|
|
166
|
+
}
|
|
167
|
+
function readMetadata(source) {
|
|
168
|
+
if (!source.startsWith("---")) return {};
|
|
169
|
+
const end = source.indexOf("\n---", 3);
|
|
170
|
+
if (end === -1) return {};
|
|
171
|
+
const out = {};
|
|
172
|
+
let inside = false;
|
|
173
|
+
for (const line of source.slice(3, end).split("\n")) {
|
|
174
|
+
if (/^metadata:\s*$/u.test(line)) {
|
|
175
|
+
inside = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (!inside) continue;
|
|
179
|
+
if (!/^\s/u.test(line)) break;
|
|
180
|
+
const match = /^ {2}([A-Za-z][A-Za-z0-9_.-]*):\s*(.*)$/u.exec(line);
|
|
181
|
+
if (!match) continue;
|
|
182
|
+
const raw = match[2] ?? "";
|
|
183
|
+
if (raw.trim() === "" || raw.trim().startsWith("#")) continue;
|
|
184
|
+
out[match[1]] = parseScalar(raw);
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
function readFrontmatterText(source) {
|
|
189
|
+
if (!source.startsWith("---")) return "";
|
|
190
|
+
const end = source.indexOf("\n---", 3);
|
|
191
|
+
return end === -1 ? "" : source.slice(3, end).replace(/^\r?\n/u, "");
|
|
192
|
+
}
|
|
193
|
+
var asString = (value) => typeof value === "string" ? value : void 0;
|
|
194
|
+
var asList = (value) => {
|
|
195
|
+
if (value === void 0) return [];
|
|
196
|
+
return typeof value === "string" ? [value] : value;
|
|
197
|
+
};
|
|
198
|
+
function readMarketplace(dir, onReject) {
|
|
199
|
+
const manifestPath = join2(dir, ".claude-plugin", "marketplace.json");
|
|
200
|
+
if (!existsSync2(manifestPath)) {
|
|
201
|
+
onReject?.({
|
|
202
|
+
path: dir,
|
|
203
|
+
reason: "no .claude-plugin/marketplace.json here"
|
|
204
|
+
});
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
let manifest;
|
|
208
|
+
try {
|
|
209
|
+
manifest = JSON.parse(
|
|
210
|
+
readFileSync2(manifestPath, "utf8")
|
|
211
|
+
);
|
|
212
|
+
} catch {
|
|
213
|
+
onReject?.({
|
|
214
|
+
path: manifestPath,
|
|
215
|
+
reason: "marketplace.json is not valid JSON"
|
|
216
|
+
});
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
const marketplace = manifest.name ?? "marketplace";
|
|
220
|
+
const tier = manifest.metadata?.tier;
|
|
221
|
+
const found = [];
|
|
222
|
+
for (const plugin of manifest.plugins ?? []) {
|
|
223
|
+
if (!plugin.name || !plugin.source) continue;
|
|
224
|
+
const skillsDir = join2(resolve(dir, plugin.source), "skills");
|
|
225
|
+
if (!existsSync2(skillsDir)) continue;
|
|
226
|
+
for (const child of readdirSync2(skillsDir, { withFileTypes: true })) {
|
|
227
|
+
if (!child.isDirectory()) continue;
|
|
228
|
+
const path = join2(skillsDir, child.name, "SKILL.md");
|
|
229
|
+
if (!existsSync2(path)) continue;
|
|
230
|
+
const source = readFileSync2(path, "utf8");
|
|
231
|
+
const front = readFrontmatter(source);
|
|
232
|
+
const description = front["description"] ?? "";
|
|
233
|
+
if (description === "") {
|
|
234
|
+
onReject?.({ path, reason: "no description" });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const metadata = readMetadata(source);
|
|
238
|
+
const ownTier = asString(metadata["tier"]);
|
|
239
|
+
if (tier !== void 0 && ownTier !== void 0 && ownTier !== tier) {
|
|
240
|
+
onReject?.({
|
|
241
|
+
path,
|
|
242
|
+
reason: `metadata.tier ${ownTier} in a ${tier} marketplace`
|
|
243
|
+
});
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const slug = front["name"] ?? child.name;
|
|
247
|
+
found.push({
|
|
248
|
+
name: `${marketplace}/${plugin.name}/${slug}`,
|
|
249
|
+
package: `${marketplace}/${plugin.name}`,
|
|
250
|
+
slug,
|
|
251
|
+
description,
|
|
252
|
+
path,
|
|
253
|
+
tier: ownTier ?? tier,
|
|
254
|
+
metadata,
|
|
255
|
+
keywords: asList(metadata["keywords"])
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return found;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/find.ts
|
|
263
|
+
var DEFAULT_FIND_LIMIT = 5;
|
|
264
|
+
function tokens(text) {
|
|
265
|
+
return normaliseIntent(text).split(" ").filter(Boolean);
|
|
266
|
+
}
|
|
267
|
+
function bigrams(text) {
|
|
268
|
+
const flat = normaliseIntent(text).replaceAll(" ", "");
|
|
269
|
+
const out = /* @__PURE__ */ new Set();
|
|
270
|
+
for (let i = 0; i + 1 < flat.length; i += 1) out.add(flat.slice(i, i + 2));
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
function lexicalScore(intent, description) {
|
|
274
|
+
const wanted = tokens(intent);
|
|
275
|
+
if (wanted.length === 0) return 0;
|
|
276
|
+
const have = new Set(tokens(description));
|
|
277
|
+
const overlap = wanted.filter((token) => have.has(token)).length / wanted.length;
|
|
278
|
+
const a = bigrams(intent);
|
|
279
|
+
const b = bigrams(description);
|
|
280
|
+
if (a.size === 0 || b.size === 0) return overlap / 2;
|
|
281
|
+
let shared = 0;
|
|
282
|
+
for (const gram of a) if (b.has(gram)) shared += 1;
|
|
283
|
+
const dice = 2 * shared / (a.size + b.size);
|
|
284
|
+
return (overlap + dice) / 2;
|
|
285
|
+
}
|
|
286
|
+
function cosine(a, b) {
|
|
287
|
+
if (a.length !== b.length) return 0;
|
|
288
|
+
let dot = 0;
|
|
289
|
+
let na = 0;
|
|
290
|
+
let nb = 0;
|
|
291
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
292
|
+
dot += a[i] * b[i];
|
|
293
|
+
na += a[i] * a[i];
|
|
294
|
+
nb += b[i] * b[i];
|
|
295
|
+
}
|
|
296
|
+
if (na === 0 || nb === 0) return 0;
|
|
297
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
298
|
+
}
|
|
299
|
+
async function find(options) {
|
|
300
|
+
const limit = options.limit ?? DEFAULT_FIND_LIMIT;
|
|
301
|
+
const semantic = options.embedder !== void 0 && options.vectors !== void 0 && options.vectors.size > 0;
|
|
302
|
+
if (semantic) {
|
|
303
|
+
const embedder = options.embedder;
|
|
304
|
+
const vectors = options.vectors;
|
|
305
|
+
let query = options.cache?.get(options.intent) ?? null;
|
|
306
|
+
if (!query) {
|
|
307
|
+
query = await embedder.embed(options.intent);
|
|
308
|
+
options.cache?.put(options.intent, query);
|
|
309
|
+
}
|
|
310
|
+
const scored = [];
|
|
311
|
+
for (const entry of options.entries) {
|
|
312
|
+
const vector = vectors.get(entry.name);
|
|
313
|
+
if (!vector) continue;
|
|
314
|
+
scored.push({
|
|
315
|
+
name: entry.name,
|
|
316
|
+
package: entry.package,
|
|
317
|
+
description: entry.description,
|
|
318
|
+
score: cosine(query, vector)
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (scored.length === options.entries.length) {
|
|
322
|
+
return {
|
|
323
|
+
ranking: "semantic",
|
|
324
|
+
matches: rank(scored, limit)
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
ranking: "lexical",
|
|
330
|
+
matches: rank(
|
|
331
|
+
options.entries.map((entry) => ({
|
|
332
|
+
name: entry.name,
|
|
333
|
+
package: entry.package,
|
|
334
|
+
description: entry.description,
|
|
335
|
+
score: lexicalScore(options.intent, entry.description)
|
|
336
|
+
})),
|
|
337
|
+
limit
|
|
338
|
+
)
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function rank(matches2, limit) {
|
|
342
|
+
return matches2.filter((match) => match.score > 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/server.ts
|
|
346
|
+
import { createHash } from "crypto";
|
|
347
|
+
import { readFileSync as readFileSync3, readdirSync as readdirSync3, realpathSync, statSync } from "fs";
|
|
348
|
+
import { dirname as dirname2, isAbsolute, join as join3, relative as relative2, resolve as resolve2, sep } from "path";
|
|
349
|
+
var READ_BUDGET_BYTES = 256 * 1024;
|
|
350
|
+
var DESCRIBE_FILE_LIMIT = 200;
|
|
351
|
+
var CATALOG_PAGE_SIZE = 8;
|
|
352
|
+
var CATALOG_PAGE_MAX = 20;
|
|
353
|
+
function matches(entry, filters) {
|
|
354
|
+
if (!filters) return true;
|
|
355
|
+
const meta = entry.metadata ?? {};
|
|
356
|
+
const field = (key) => {
|
|
357
|
+
const value = meta[key];
|
|
358
|
+
return typeof value === "string" ? value : void 0;
|
|
359
|
+
};
|
|
360
|
+
if (filters.tier !== void 0 && entry.tier !== filters.tier) return false;
|
|
361
|
+
if (filters.package !== void 0 && entry.package !== filters.package)
|
|
362
|
+
return false;
|
|
363
|
+
if (filters.level !== void 0 && field("level") !== filters.level)
|
|
364
|
+
return false;
|
|
365
|
+
if (filters.domain !== void 0 && field("domain") !== filters.domain)
|
|
366
|
+
return false;
|
|
367
|
+
if (filters.medium !== void 0 && field("medium") !== filters.medium)
|
|
368
|
+
return false;
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
372
|
+
function listFiles(root) {
|
|
373
|
+
const files = [];
|
|
374
|
+
const stack = [root];
|
|
375
|
+
while (stack.length > 0) {
|
|
376
|
+
const dir = stack.pop();
|
|
377
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
378
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
379
|
+
const full = join3(dir, entry.name);
|
|
380
|
+
if (entry.isDirectory()) {
|
|
381
|
+
stack.push(full);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (!entry.isFile()) continue;
|
|
385
|
+
if (files.length >= DESCRIBE_FILE_LIMIT)
|
|
386
|
+
return { files, truncated: true };
|
|
387
|
+
const bytes = readFileSync3(full);
|
|
388
|
+
files.push({
|
|
389
|
+
path: relative2(root, full).split(sep).join("/"),
|
|
390
|
+
bytes: bytes.length,
|
|
391
|
+
sha256: sha256(bytes)
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
396
|
+
return { files, truncated: false };
|
|
397
|
+
}
|
|
398
|
+
function createSkillTools(options) {
|
|
399
|
+
const load = options.loadCatalog ?? readCatalog;
|
|
400
|
+
let entries = null;
|
|
401
|
+
const catalog = () => {
|
|
402
|
+
entries ??= load({
|
|
403
|
+
roots: options.roots,
|
|
404
|
+
scopes: options.scopes,
|
|
405
|
+
marketplaces: options.marketplaces,
|
|
406
|
+
onReject: options.onReject
|
|
407
|
+
});
|
|
408
|
+
return entries;
|
|
409
|
+
};
|
|
410
|
+
return {
|
|
411
|
+
/**
|
|
412
|
+
* Grouped, and without bodies. The cheap overview and the search's miss
|
|
413
|
+
* path. Paged by package so a large catalog never arrives in one answer.
|
|
414
|
+
*/
|
|
415
|
+
catalog(request = {}) {
|
|
416
|
+
const byPackage = /* @__PURE__ */ new Map();
|
|
417
|
+
let count = 0;
|
|
418
|
+
for (const entry of catalog()) {
|
|
419
|
+
if (!matches(entry, { tier: request.tier })) continue;
|
|
420
|
+
count += 1;
|
|
421
|
+
const list = byPackage.get(entry.package) ?? [];
|
|
422
|
+
list.push(
|
|
423
|
+
entry.tier === void 0 ? { name: entry.name, description: entry.description } : {
|
|
424
|
+
name: entry.name,
|
|
425
|
+
description: entry.description,
|
|
426
|
+
tier: entry.tier
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
byPackage.set(entry.package, list);
|
|
430
|
+
}
|
|
431
|
+
const groups = [...byPackage.entries()].sort(
|
|
432
|
+
([a], [b]) => a.localeCompare(b)
|
|
433
|
+
);
|
|
434
|
+
const size = Math.min(
|
|
435
|
+
Math.max(request.pageSize ?? CATALOG_PAGE_SIZE, 1),
|
|
436
|
+
CATALOG_PAGE_MAX
|
|
437
|
+
);
|
|
438
|
+
const totalPages = Math.max(1, Math.ceil(groups.length / size));
|
|
439
|
+
const page = Math.min(Math.max(request.page ?? 1, 1), totalPages);
|
|
440
|
+
return {
|
|
441
|
+
count,
|
|
442
|
+
page,
|
|
443
|
+
totalPages,
|
|
444
|
+
hasNext: page < totalPages,
|
|
445
|
+
packages: groups.slice((page - 1) * size, page * size).map(([pkg, skills]) => ({ package: pkg, skills }))
|
|
446
|
+
};
|
|
447
|
+
},
|
|
448
|
+
/** Ranked names. Returns; never loads a body. */
|
|
449
|
+
find(intent, limit, filters) {
|
|
450
|
+
const scoped = catalog().filter((entry) => matches(entry, filters)).map(
|
|
451
|
+
(entry) => entry.keywords && entry.keywords.length > 0 ? {
|
|
452
|
+
...entry,
|
|
453
|
+
description: `${entry.description} ${entry.keywords.join(" ")}`
|
|
454
|
+
} : entry
|
|
455
|
+
);
|
|
456
|
+
return find({
|
|
457
|
+
entries: scoped,
|
|
458
|
+
intent,
|
|
459
|
+
limit,
|
|
460
|
+
embedder: options.embedder,
|
|
461
|
+
cache: options.cache,
|
|
462
|
+
vectors: options.vectors
|
|
463
|
+
}).then((result) => ({
|
|
464
|
+
...result,
|
|
465
|
+
matches: result.matches.map((match) => ({
|
|
466
|
+
...match,
|
|
467
|
+
description: catalog().find((entry) => entry.name === match.name)?.description ?? match.description
|
|
468
|
+
}))
|
|
469
|
+
}));
|
|
470
|
+
},
|
|
471
|
+
/** Every property of one skill and the files it carries, without bodies. */
|
|
472
|
+
describe(name) {
|
|
473
|
+
const entry = catalog().find((candidate) => candidate.name === name);
|
|
474
|
+
if (!entry) return null;
|
|
475
|
+
const source = readFileSync3(entry.path, "utf8");
|
|
476
|
+
const listing = listFiles(dirname2(entry.path));
|
|
477
|
+
return {
|
|
478
|
+
name: entry.name,
|
|
479
|
+
package: entry.package,
|
|
480
|
+
description: entry.description,
|
|
481
|
+
tier: entry.tier,
|
|
482
|
+
metadata: entry.metadata ?? {},
|
|
483
|
+
frontmatter: readFrontmatterText(source),
|
|
484
|
+
files: listing.files,
|
|
485
|
+
filesTruncated: listing.truncated
|
|
486
|
+
};
|
|
487
|
+
},
|
|
488
|
+
/**
|
|
489
|
+
* One file inside a skill's directory. Paths that leave the directory,
|
|
490
|
+
* directly or through a link, are refused; nothing is ever executed.
|
|
491
|
+
*/
|
|
492
|
+
read(name, path = "SKILL.md") {
|
|
493
|
+
const entry = catalog().find((candidate) => candidate.name === name);
|
|
494
|
+
if (!entry) return { ok: false, error: "unknown_skill", detail: name };
|
|
495
|
+
if (isAbsolute(path) || path.split(/[\\/]/u).includes("..")) {
|
|
496
|
+
return { ok: false, error: "path_outside_skill", detail: path };
|
|
497
|
+
}
|
|
498
|
+
const root = realpathSync(dirname2(entry.path));
|
|
499
|
+
let target;
|
|
500
|
+
try {
|
|
501
|
+
target = realpathSync(resolve2(root, path));
|
|
502
|
+
} catch {
|
|
503
|
+
return { ok: false, error: "no_such_file", detail: path };
|
|
504
|
+
}
|
|
505
|
+
if (target !== root && !target.startsWith(root + sep)) {
|
|
506
|
+
return { ok: false, error: "path_outside_skill", detail: path };
|
|
507
|
+
}
|
|
508
|
+
const size = statSync(target).size;
|
|
509
|
+
if (size > READ_BUDGET_BYTES) {
|
|
510
|
+
return {
|
|
511
|
+
ok: false,
|
|
512
|
+
error: "over_budget",
|
|
513
|
+
detail: `${size} bytes > ${READ_BUDGET_BYTES}`
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const bytes = readFileSync3(target);
|
|
517
|
+
return {
|
|
518
|
+
ok: true,
|
|
519
|
+
name: entry.name,
|
|
520
|
+
path: relative2(root, target).split(sep).join("/"),
|
|
521
|
+
bytes: bytes.length,
|
|
522
|
+
sha256: sha256(bytes),
|
|
523
|
+
text: bytes.toString("utf8")
|
|
524
|
+
};
|
|
525
|
+
},
|
|
526
|
+
/** One body, at the moment it is needed. */
|
|
527
|
+
call(name) {
|
|
528
|
+
const entry = catalog().find((candidate) => candidate.name === name);
|
|
529
|
+
if (!entry) return null;
|
|
530
|
+
return {
|
|
531
|
+
name: entry.name,
|
|
532
|
+
body: readBody(readFileSync3(entry.path, "utf8"))
|
|
533
|
+
};
|
|
534
|
+
},
|
|
535
|
+
/** Drops the memoised catalog, so the next call re-reads the tree. */
|
|
536
|
+
refresh() {
|
|
537
|
+
entries = null;
|
|
538
|
+
},
|
|
539
|
+
entries: () => catalog().map(withoutBody)
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/mcp.ts
|
|
544
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
545
|
+
import { z } from "zod";
|
|
546
|
+
var SERVER_NAME = "pmcp";
|
|
547
|
+
var readOnly = {
|
|
548
|
+
readOnlyHint: true,
|
|
549
|
+
destructiveHint: false,
|
|
550
|
+
idempotentHint: true,
|
|
551
|
+
openWorldHint: false
|
|
552
|
+
};
|
|
553
|
+
var asText = (value) => ({
|
|
554
|
+
content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
555
|
+
});
|
|
556
|
+
function registerSkillTools(server, tools) {
|
|
557
|
+
server.registerTool(
|
|
558
|
+
"skill_catalog",
|
|
559
|
+
{
|
|
560
|
+
title: "List installed skills without their bodies",
|
|
561
|
+
description: "Every skill the installed packages and marketplaces ship, grouped by package or plugin, with names, one-line descriptions and tier, and no bodies. Paged by group. Use it to see what exists, or when skill_find returns nothing.",
|
|
562
|
+
inputSchema: z.object({
|
|
563
|
+
page: z.number().int().min(1).optional(),
|
|
564
|
+
pageSize: z.number().int().min(1).max(20).optional(),
|
|
565
|
+
tier: z.enum(["open", "free", "paid"]).optional()
|
|
566
|
+
}),
|
|
567
|
+
annotations: readOnly
|
|
568
|
+
},
|
|
569
|
+
async (request) => asText(tools.catalog(request))
|
|
570
|
+
);
|
|
571
|
+
server.registerTool(
|
|
572
|
+
"skill_find",
|
|
573
|
+
{
|
|
574
|
+
title: "Find skills by describing the problem",
|
|
575
|
+
description: "Ranked skill names for an intent. Describe the problem in a full sentence in English rather than a keyword: a single word matches far worse, and the ranking is over descriptions. Returns names only -- read one with skill_call.",
|
|
576
|
+
inputSchema: z.object({
|
|
577
|
+
intent: z.string().min(1).max(256).describe("What you are trying to do, as an English sentence."),
|
|
578
|
+
limit: z.number().int().min(1).max(25).optional(),
|
|
579
|
+
filters: z.object({
|
|
580
|
+
tier: z.enum(["open", "free", "paid"]).optional(),
|
|
581
|
+
package: z.string().optional(),
|
|
582
|
+
level: z.string().optional(),
|
|
583
|
+
domain: z.string().optional(),
|
|
584
|
+
medium: z.string().optional()
|
|
585
|
+
}).optional().describe("Exact-match filters over the catalog before ranking.")
|
|
586
|
+
}),
|
|
587
|
+
annotations: readOnly
|
|
588
|
+
},
|
|
589
|
+
async ({ intent, limit, filters }) => asText(await tools.find(intent, limit, filters))
|
|
590
|
+
);
|
|
591
|
+
server.registerTool(
|
|
592
|
+
"skill_describe",
|
|
593
|
+
{
|
|
594
|
+
title: "Check one skill's properties before loading it",
|
|
595
|
+
description: "Frontmatter, metadata (tier, level, requirements, licence, approval), and the files the skill carries with sizes and sha256, without any body. Use it to decide between candidates before skill_call.",
|
|
596
|
+
inputSchema: z.object({
|
|
597
|
+
name: z.string().min(1).describe("The skill name skill_find or skill_catalog returned.")
|
|
598
|
+
}),
|
|
599
|
+
annotations: readOnly
|
|
600
|
+
},
|
|
601
|
+
async ({ name }) => {
|
|
602
|
+
const described = tools.describe(name);
|
|
603
|
+
if (!described) {
|
|
604
|
+
return {
|
|
605
|
+
...asText({
|
|
606
|
+
error: "unknown_skill",
|
|
607
|
+
name,
|
|
608
|
+
hint: "Call skill_catalog for the names that exist."
|
|
609
|
+
}),
|
|
610
|
+
isError: true
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
return asText(described);
|
|
614
|
+
}
|
|
615
|
+
);
|
|
616
|
+
server.registerTool(
|
|
617
|
+
"skill_read",
|
|
618
|
+
{
|
|
619
|
+
title: "Read one file a skill carries",
|
|
620
|
+
description: "A reference or script file inside one skill's directory, as text, with its sha256. Read only when the skill body points at the file. Scripts are returned, never run. Files over 256 KiB and paths outside the skill are refused.",
|
|
621
|
+
inputSchema: z.object({
|
|
622
|
+
name: z.string().min(1),
|
|
623
|
+
path: z.string().min(1).optional().describe(
|
|
624
|
+
"Path relative to the skill directory, as skill_describe lists it. Defaults to SKILL.md."
|
|
625
|
+
)
|
|
626
|
+
}),
|
|
627
|
+
annotations: readOnly
|
|
628
|
+
},
|
|
629
|
+
async ({ name, path }) => {
|
|
630
|
+
const result = tools.read(name, path);
|
|
631
|
+
if (!result.ok) return { ...asText(result), isError: true };
|
|
632
|
+
const { text, ...header } = result;
|
|
633
|
+
return {
|
|
634
|
+
content: [
|
|
635
|
+
{ type: "text", text: JSON.stringify(header) },
|
|
636
|
+
{ type: "text", text }
|
|
637
|
+
]
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
server.registerTool(
|
|
642
|
+
"skill_call",
|
|
643
|
+
{
|
|
644
|
+
title: "Read one skill",
|
|
645
|
+
description: "The body of a single skill, by the name skill_find or skill_catalog returned. This is the only call that costs a document.",
|
|
646
|
+
inputSchema: z.object({
|
|
647
|
+
name: z.string().min(1).describe("The skill name, as <package>/<slug>.")
|
|
648
|
+
}),
|
|
649
|
+
annotations: readOnly
|
|
650
|
+
},
|
|
651
|
+
async ({ name }) => {
|
|
652
|
+
const found = tools.call(name);
|
|
653
|
+
if (!found) {
|
|
654
|
+
return {
|
|
655
|
+
...asText({
|
|
656
|
+
error: "unknown_skill",
|
|
657
|
+
name,
|
|
658
|
+
hint: "Call skill_catalog for the names that exist."
|
|
659
|
+
}),
|
|
660
|
+
isError: true
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
return { content: [{ type: "text", text: found.body }] };
|
|
664
|
+
}
|
|
665
|
+
);
|
|
666
|
+
return server;
|
|
667
|
+
}
|
|
668
|
+
function createSkillServer(options) {
|
|
669
|
+
const server = new McpServer({ name: SERVER_NAME, version: "0.2.0" });
|
|
670
|
+
return registerSkillTools(server, createSkillTools(options));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export {
|
|
674
|
+
normaliseIntent,
|
|
675
|
+
readMetadata,
|
|
676
|
+
readFrontmatterText,
|
|
677
|
+
readMarketplace,
|
|
678
|
+
SKIPPED_DIRS,
|
|
679
|
+
MAX_SKILL_DEPTH,
|
|
680
|
+
readFrontmatter,
|
|
681
|
+
readBody,
|
|
682
|
+
readCatalog,
|
|
683
|
+
withoutBody,
|
|
684
|
+
displayPath,
|
|
685
|
+
DEFAULT_FIND_LIMIT,
|
|
686
|
+
lexicalScore,
|
|
687
|
+
cosine,
|
|
688
|
+
find,
|
|
689
|
+
READ_BUDGET_BYTES,
|
|
690
|
+
DESCRIBE_FILE_LIMIT,
|
|
691
|
+
CATALOG_PAGE_SIZE,
|
|
692
|
+
CATALOG_PAGE_MAX,
|
|
693
|
+
createSkillTools,
|
|
694
|
+
SERVER_NAME,
|
|
695
|
+
registerSkillTools,
|
|
696
|
+
createSkillServer
|
|
697
|
+
};
|
|
698
|
+
//# sourceMappingURL=chunk-3NNRQ5A4.js.map
|