skillli 0.1.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/skills/skillli/SKILL.md +71 -0
- package/.claude/skills/skillli/references/skill-format-spec.md +62 -0
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/cli/index.cjs +1253 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1230 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +1025 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +191 -0
- package/dist/index.d.ts +191 -0
- package/dist/index.js +961 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.cjs +813 -0
- package/dist/mcp/index.cjs.map +1 -0
- package/dist/mcp/index.d.cts +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +790 -0
- package/dist/mcp/index.js.map +1 -0
- package/package.json +91 -0
- package/templates/skill/SKILL.md.hbs +33 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
// src/core/parser.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
|
|
5
|
+
// src/core/schema.ts
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
var SkillCategorySchema = z.enum([
|
|
8
|
+
"development",
|
|
9
|
+
"creative",
|
|
10
|
+
"enterprise",
|
|
11
|
+
"data",
|
|
12
|
+
"devops",
|
|
13
|
+
"other"
|
|
14
|
+
]);
|
|
15
|
+
var TrustLevelSchema = z.enum(["community", "verified", "official"]);
|
|
16
|
+
var SkillMetadataSchema = z.object({
|
|
17
|
+
name: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/, "Must be lowercase alphanumeric with hyphens, not starting/ending with hyphen"),
|
|
18
|
+
version: z.string().regex(/^\d+\.\d+\.\d+$/, "Must be valid semver (e.g. 1.0.0)"),
|
|
19
|
+
description: z.string().min(10).max(500),
|
|
20
|
+
author: z.string().min(1),
|
|
21
|
+
license: z.string().min(1),
|
|
22
|
+
tags: z.array(z.string().min(1).max(50)).min(1).max(20),
|
|
23
|
+
category: SkillCategorySchema,
|
|
24
|
+
repository: z.string().url().optional(),
|
|
25
|
+
homepage: z.string().url().optional(),
|
|
26
|
+
"min-skillli-version": z.string().optional(),
|
|
27
|
+
"trust-level": TrustLevelSchema.default("community"),
|
|
28
|
+
checksum: z.string().optional(),
|
|
29
|
+
"disable-model-invocation": z.boolean().default(false),
|
|
30
|
+
"user-invocable": z.boolean().default(true)
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// src/core/errors.ts
|
|
34
|
+
var SkillliError = class extends Error {
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "SkillliError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var SkillValidationError = class extends SkillliError {
|
|
41
|
+
details;
|
|
42
|
+
constructor(message, details = []) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "SkillValidationError";
|
|
45
|
+
this.details = details;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var SkillNotFoundError = class extends SkillliError {
|
|
49
|
+
constructor(skillName) {
|
|
50
|
+
super(`Skill not found: ${skillName}`);
|
|
51
|
+
this.name = "SkillNotFoundError";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var RegistryError = class extends SkillliError {
|
|
55
|
+
constructor(message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "RegistryError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var InstallError = class extends SkillliError {
|
|
61
|
+
constructor(message) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = "InstallError";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/core/parser.ts
|
|
68
|
+
function normalizeMetadata(raw) {
|
|
69
|
+
return {
|
|
70
|
+
name: raw.name,
|
|
71
|
+
version: raw.version,
|
|
72
|
+
description: raw.description,
|
|
73
|
+
author: raw.author,
|
|
74
|
+
license: raw.license,
|
|
75
|
+
tags: raw.tags,
|
|
76
|
+
category: raw.category,
|
|
77
|
+
repository: raw.repository,
|
|
78
|
+
homepage: raw.homepage,
|
|
79
|
+
minSkillliVersion: raw["min-skillli-version"],
|
|
80
|
+
trustLevel: raw["trust-level"],
|
|
81
|
+
checksum: raw.checksum,
|
|
82
|
+
disableModelInvocation: raw["disable-model-invocation"],
|
|
83
|
+
userInvocable: raw["user-invocable"]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function validateMetadata(data) {
|
|
87
|
+
const result = SkillMetadataSchema.safeParse(data);
|
|
88
|
+
if (!result.success) {
|
|
89
|
+
const details = result.error.issues.map(
|
|
90
|
+
(issue) => `${issue.path.join(".")}: ${issue.message}`
|
|
91
|
+
);
|
|
92
|
+
throw new SkillValidationError("Invalid skill metadata", details);
|
|
93
|
+
}
|
|
94
|
+
return normalizeMetadata(result.data);
|
|
95
|
+
}
|
|
96
|
+
function parseSkillContent(content, filePath = "<inline>") {
|
|
97
|
+
const { data, content: body, matter: rawFrontmatter } = matter(content);
|
|
98
|
+
const metadata = validateMetadata(data);
|
|
99
|
+
return {
|
|
100
|
+
metadata,
|
|
101
|
+
content: body.trim(),
|
|
102
|
+
rawFrontmatter: rawFrontmatter || "",
|
|
103
|
+
filePath
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function parseSkillFile(filePath) {
|
|
107
|
+
const content = await readFile(filePath, "utf-8");
|
|
108
|
+
return parseSkillContent(content, filePath);
|
|
109
|
+
}
|
|
110
|
+
function extractManifest(skill) {
|
|
111
|
+
const { metadata } = skill;
|
|
112
|
+
return {
|
|
113
|
+
name: metadata.name,
|
|
114
|
+
version: metadata.version,
|
|
115
|
+
description: metadata.description,
|
|
116
|
+
author: metadata.author,
|
|
117
|
+
license: metadata.license,
|
|
118
|
+
tags: metadata.tags,
|
|
119
|
+
category: metadata.category,
|
|
120
|
+
repository: metadata.repository,
|
|
121
|
+
trust_level: metadata.trustLevel,
|
|
122
|
+
checksum: metadata.checksum,
|
|
123
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
124
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/core/search.ts
|
|
129
|
+
function tokenize(text) {
|
|
130
|
+
return text.toLowerCase().split(/[\s\-_,./]+/).filter((t) => t.length > 1);
|
|
131
|
+
}
|
|
132
|
+
function textScore(tokens, text) {
|
|
133
|
+
const lower = text.toLowerCase();
|
|
134
|
+
let score = 0;
|
|
135
|
+
for (const token of tokens) {
|
|
136
|
+
if (lower.includes(token)) score += 1;
|
|
137
|
+
}
|
|
138
|
+
return score;
|
|
139
|
+
}
|
|
140
|
+
function scoreSkill(entry, queryTokens, options) {
|
|
141
|
+
let score = 0;
|
|
142
|
+
const matchedOn = [];
|
|
143
|
+
const nameScore = textScore(queryTokens, entry.name);
|
|
144
|
+
if (nameScore > 0) {
|
|
145
|
+
score += nameScore * 5;
|
|
146
|
+
matchedOn.push("name");
|
|
147
|
+
}
|
|
148
|
+
if (entry.name === options.query.toLowerCase()) {
|
|
149
|
+
score += 10;
|
|
150
|
+
}
|
|
151
|
+
const descScore = textScore(queryTokens, entry.description);
|
|
152
|
+
if (descScore > 0) {
|
|
153
|
+
score += descScore * 2;
|
|
154
|
+
matchedOn.push("description");
|
|
155
|
+
}
|
|
156
|
+
for (const token of queryTokens) {
|
|
157
|
+
if (entry.tags.some((t) => t.toLowerCase() === token)) {
|
|
158
|
+
score += 4;
|
|
159
|
+
if (!matchedOn.includes("tags")) matchedOn.push("tags");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (options.tags) {
|
|
163
|
+
for (const tag of options.tags) {
|
|
164
|
+
if (entry.tags.includes(tag)) {
|
|
165
|
+
score += 3;
|
|
166
|
+
if (!matchedOn.includes("tags")) matchedOn.push("tags");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (options.category && entry.category === options.category) {
|
|
171
|
+
score += 2;
|
|
172
|
+
matchedOn.push("category");
|
|
173
|
+
}
|
|
174
|
+
if (matchedOn.length > 0) {
|
|
175
|
+
if (entry.trustLevel === "official") score += 3;
|
|
176
|
+
if (entry.trustLevel === "verified") score += 1.5;
|
|
177
|
+
score += entry.rating.average * 0.5;
|
|
178
|
+
if (entry.downloads > 0) {
|
|
179
|
+
score += Math.log10(entry.downloads) * 0.5;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { score, matchedOn };
|
|
183
|
+
}
|
|
184
|
+
function search(index, options) {
|
|
185
|
+
const queryTokens = tokenize(options.query);
|
|
186
|
+
const results = [];
|
|
187
|
+
for (const entry of Object.values(index.skills)) {
|
|
188
|
+
if (options.category && entry.category !== options.category) continue;
|
|
189
|
+
if (options.trustLevel && entry.trustLevel !== options.trustLevel) continue;
|
|
190
|
+
if (options.minRating && entry.rating.average < options.minRating) continue;
|
|
191
|
+
const { score, matchedOn } = scoreSkill(entry, queryTokens, options);
|
|
192
|
+
if (score > 0) {
|
|
193
|
+
results.push({ skill: entry, relevanceScore: score, matchedOn });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
results.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
197
|
+
const offset = options.offset ?? 0;
|
|
198
|
+
const limit = options.limit ?? 20;
|
|
199
|
+
return results.slice(offset, offset + limit);
|
|
200
|
+
}
|
|
201
|
+
function searchByTags(index, tags) {
|
|
202
|
+
return search(index, { query: tags.join(" "), tags });
|
|
203
|
+
}
|
|
204
|
+
function searchByCategory(index, category) {
|
|
205
|
+
return search(index, { query: "", category });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/core/installer.ts
|
|
209
|
+
import { mkdir as mkdir2, rm, symlink } from "fs/promises";
|
|
210
|
+
import { existsSync as existsSync3 } from "fs";
|
|
211
|
+
import { join as join3 } from "path";
|
|
212
|
+
import { execSync } from "child_process";
|
|
213
|
+
|
|
214
|
+
// src/core/constants.ts
|
|
215
|
+
import { homedir } from "os";
|
|
216
|
+
import { join } from "path";
|
|
217
|
+
var SKILLLI_DIR = join(homedir(), ".skillli");
|
|
218
|
+
var LOCAL_INDEX_PATH = join(SKILLLI_DIR, "index.json");
|
|
219
|
+
var CONFIG_PATH = join(SKILLLI_DIR, "config.json");
|
|
220
|
+
var SKILLS_DIR = join(SKILLLI_DIR, "skills");
|
|
221
|
+
var CACHE_DIR = join(SKILLLI_DIR, "cache");
|
|
222
|
+
var DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/skillli/registry/main/index.json";
|
|
223
|
+
var SKILL_FILENAME = "SKILL.md";
|
|
224
|
+
var MAX_SKILL_SIZE_BYTES = 5 * 1024 * 1024;
|
|
225
|
+
var MAX_SKILL_MD_LINES = 500;
|
|
226
|
+
var VERSION = "0.1.0";
|
|
227
|
+
|
|
228
|
+
// src/core/local-store.ts
|
|
229
|
+
import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
230
|
+
import { existsSync } from "fs";
|
|
231
|
+
async function ensureDir() {
|
|
232
|
+
for (const dir of [SKILLLI_DIR, SKILLS_DIR, CACHE_DIR]) {
|
|
233
|
+
if (!existsSync(dir)) {
|
|
234
|
+
await mkdir(dir, { recursive: true });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function defaultConfig() {
|
|
239
|
+
return {
|
|
240
|
+
installedSkills: {},
|
|
241
|
+
registryUrl: DEFAULT_REGISTRY_URL,
|
|
242
|
+
lastSync: ""
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function defaultIndex() {
|
|
246
|
+
return {
|
|
247
|
+
version: "1.0.0",
|
|
248
|
+
lastUpdated: "",
|
|
249
|
+
skills: {}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
async function getConfig() {
|
|
253
|
+
await ensureDir();
|
|
254
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
255
|
+
const config = defaultConfig();
|
|
256
|
+
await saveConfig(config);
|
|
257
|
+
return config;
|
|
258
|
+
}
|
|
259
|
+
const raw = await readFile2(CONFIG_PATH, "utf-8");
|
|
260
|
+
return JSON.parse(raw);
|
|
261
|
+
}
|
|
262
|
+
async function saveConfig(config) {
|
|
263
|
+
await ensureDir();
|
|
264
|
+
await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
265
|
+
}
|
|
266
|
+
async function getLocalIndex() {
|
|
267
|
+
await ensureDir();
|
|
268
|
+
if (!existsSync(LOCAL_INDEX_PATH)) {
|
|
269
|
+
const index = defaultIndex();
|
|
270
|
+
await saveLocalIndex(index);
|
|
271
|
+
return index;
|
|
272
|
+
}
|
|
273
|
+
const raw = await readFile2(LOCAL_INDEX_PATH, "utf-8");
|
|
274
|
+
return JSON.parse(raw);
|
|
275
|
+
}
|
|
276
|
+
async function saveLocalIndex(index) {
|
|
277
|
+
await ensureDir();
|
|
278
|
+
await writeFile(LOCAL_INDEX_PATH, JSON.stringify(index, null, 2));
|
|
279
|
+
}
|
|
280
|
+
async function getInstalledSkills() {
|
|
281
|
+
const config = await getConfig();
|
|
282
|
+
return Object.values(config.installedSkills);
|
|
283
|
+
}
|
|
284
|
+
async function markInstalled(skill) {
|
|
285
|
+
const config = await getConfig();
|
|
286
|
+
config.installedSkills[skill.name] = skill;
|
|
287
|
+
await saveConfig(config);
|
|
288
|
+
}
|
|
289
|
+
async function markUninstalled(name) {
|
|
290
|
+
const config = await getConfig();
|
|
291
|
+
delete config.installedSkills[name];
|
|
292
|
+
await saveConfig(config);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/core/registry.ts
|
|
296
|
+
async function fetchIndex(registryUrl) {
|
|
297
|
+
const config = await getConfig();
|
|
298
|
+
const url = registryUrl ?? config.registryUrl ?? DEFAULT_REGISTRY_URL;
|
|
299
|
+
try {
|
|
300
|
+
const res = await fetch(url);
|
|
301
|
+
if (!res.ok) {
|
|
302
|
+
throw new RegistryError(`Failed to fetch registry: ${res.status} ${res.statusText}`);
|
|
303
|
+
}
|
|
304
|
+
const index = await res.json();
|
|
305
|
+
index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
306
|
+
await saveLocalIndex(index);
|
|
307
|
+
return index;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error instanceof RegistryError) throw error;
|
|
310
|
+
const localIndex = await getLocalIndex();
|
|
311
|
+
if (Object.keys(localIndex.skills).length > 0) {
|
|
312
|
+
return localIndex;
|
|
313
|
+
}
|
|
314
|
+
throw new RegistryError(`Cannot reach registry at ${url}: ${error}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function getSkillEntry(name) {
|
|
318
|
+
const index = await getLocalIndex();
|
|
319
|
+
const entry = index.skills[name];
|
|
320
|
+
if (!entry) {
|
|
321
|
+
throw new SkillNotFoundError(name);
|
|
322
|
+
}
|
|
323
|
+
return entry;
|
|
324
|
+
}
|
|
325
|
+
async function syncIndex() {
|
|
326
|
+
return fetchIndex();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/core/safeguards.ts
|
|
330
|
+
import { stat } from "fs/promises";
|
|
331
|
+
import { join as join2 } from "path";
|
|
332
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
333
|
+
var PROHIBITED_PATTERNS = [
|
|
334
|
+
{ pattern: /\beval\s*\(/, label: "eval()" },
|
|
335
|
+
{ pattern: /\bexec\s*\(/, label: "exec()" },
|
|
336
|
+
{ pattern: /\bexecSync\s*\(/, label: "execSync()" },
|
|
337
|
+
{ pattern: /rm\s+-rf\s+\//, label: "rm -rf /" },
|
|
338
|
+
{ pattern: /\bchild_process\b/, label: "child_process" },
|
|
339
|
+
{ pattern: /\bProcess\.kill\b/i, label: "Process.kill" },
|
|
340
|
+
{ pattern: /password\s*[:=]\s*['"][^'"]+['"]/, label: "hardcoded password" },
|
|
341
|
+
{ pattern: /api[_-]?key\s*[:=]\s*['"][^'"]+['"]/, label: "hardcoded API key" },
|
|
342
|
+
{ pattern: /[A-Za-z0-9+/]{100,}={0,2}/, label: "large base64 blob" }
|
|
343
|
+
];
|
|
344
|
+
var ALLOWED_SCRIPT_EXTENSIONS = [".sh", ".py", ".js", ".ts"];
|
|
345
|
+
function checkSchema(skill) {
|
|
346
|
+
return {
|
|
347
|
+
name: "schema-validation",
|
|
348
|
+
passed: true,
|
|
349
|
+
severity: "info",
|
|
350
|
+
message: "SKILL.md metadata is valid"
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function checkLineCount(content) {
|
|
354
|
+
const lines = content.split("\n").length;
|
|
355
|
+
const passed = lines <= MAX_SKILL_MD_LINES;
|
|
356
|
+
return {
|
|
357
|
+
name: "line-count",
|
|
358
|
+
passed,
|
|
359
|
+
severity: passed ? "info" : "warning",
|
|
360
|
+
message: passed ? `SKILL.md has ${lines} lines (max ${MAX_SKILL_MD_LINES})` : `SKILL.md has ${lines} lines, exceeds max of ${MAX_SKILL_MD_LINES}`
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function checkProhibitedPatterns(content) {
|
|
364
|
+
const found = [];
|
|
365
|
+
for (const { pattern, label } of PROHIBITED_PATTERNS) {
|
|
366
|
+
if (pattern.test(content)) {
|
|
367
|
+
found.push(label);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const passed = found.length === 0;
|
|
371
|
+
return {
|
|
372
|
+
name: "prohibited-patterns",
|
|
373
|
+
passed,
|
|
374
|
+
severity: passed ? "info" : "error",
|
|
375
|
+
message: passed ? "No prohibited patterns detected" : `Prohibited patterns found: ${found.join(", ")}`
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function checkScriptSafety(skillDir) {
|
|
379
|
+
const scriptsDir = join2(skillDir, "scripts");
|
|
380
|
+
if (!existsSync2(scriptsDir)) {
|
|
381
|
+
return {
|
|
382
|
+
name: "script-safety",
|
|
383
|
+
passed: true,
|
|
384
|
+
severity: "info",
|
|
385
|
+
message: "No scripts directory found"
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const badFiles = [];
|
|
389
|
+
const files = readdirSync(scriptsDir, { recursive: true });
|
|
390
|
+
for (const file of files) {
|
|
391
|
+
const ext = "." + file.split(".").pop();
|
|
392
|
+
if (!ALLOWED_SCRIPT_EXTENSIONS.includes(ext)) {
|
|
393
|
+
badFiles.push(file);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const passed = badFiles.length === 0;
|
|
397
|
+
return {
|
|
398
|
+
name: "script-safety",
|
|
399
|
+
passed,
|
|
400
|
+
severity: passed ? "info" : "error",
|
|
401
|
+
message: passed ? "All scripts use allowed extensions" : `Disallowed script types: ${badFiles.join(", ")}`
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
async function checkFileSize(skillDir) {
|
|
405
|
+
let totalSize = 0;
|
|
406
|
+
const entries = readdirSync(skillDir, { recursive: true, withFileTypes: true });
|
|
407
|
+
for (const entry of entries) {
|
|
408
|
+
if (entry.isFile()) {
|
|
409
|
+
const fullPath = join2(entry.parentPath ?? skillDir, entry.name);
|
|
410
|
+
const s = await stat(fullPath);
|
|
411
|
+
totalSize += s.size;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const passed = totalSize <= MAX_SKILL_SIZE_BYTES;
|
|
415
|
+
return {
|
|
416
|
+
name: "file-size",
|
|
417
|
+
passed,
|
|
418
|
+
severity: passed ? "info" : "warning",
|
|
419
|
+
message: passed ? `Total size: ${(totalSize / 1024).toFixed(1)}KB (max ${MAX_SKILL_SIZE_BYTES / 1024 / 1024}MB)` : `Total size ${(totalSize / 1024 / 1024).toFixed(1)}MB exceeds max of ${MAX_SKILL_SIZE_BYTES / 1024 / 1024}MB`
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
async function runSafeguards(skill, skillDir) {
|
|
423
|
+
const checks = [];
|
|
424
|
+
checks.push(checkSchema(skill));
|
|
425
|
+
checks.push(checkLineCount(skill.content));
|
|
426
|
+
checks.push(checkProhibitedPatterns(skill.content + skill.rawFrontmatter));
|
|
427
|
+
if (skillDir) {
|
|
428
|
+
checks.push(checkScriptSafety(skillDir));
|
|
429
|
+
checks.push(await checkFileSize(skillDir));
|
|
430
|
+
}
|
|
431
|
+
const passed = checks.every((c) => c.passed || c.severity !== "error");
|
|
432
|
+
const score = computeTrustScore(skill);
|
|
433
|
+
return { passed, score, checks };
|
|
434
|
+
}
|
|
435
|
+
function computeTrustScore(skill, registryEntry) {
|
|
436
|
+
let score = 0;
|
|
437
|
+
if (skill.metadata.repository) score += 10;
|
|
438
|
+
if (skill.metadata.license) score += 10;
|
|
439
|
+
if (skill.metadata.trustLevel === "verified") score += 15;
|
|
440
|
+
if (skill.metadata.trustLevel === "official") score += 20;
|
|
441
|
+
if (registryEntry && registryEntry.rating.average >= 3.5) score += 15;
|
|
442
|
+
if (registryEntry && registryEntry.downloads > 100) score += 5;
|
|
443
|
+
if (registryEntry && registryEntry.downloads > 1e3) score += 5;
|
|
444
|
+
const patternCheck = checkProhibitedPatterns(skill.content);
|
|
445
|
+
const lineCheck = checkLineCount(skill.content);
|
|
446
|
+
if (patternCheck.passed) score += 20;
|
|
447
|
+
if (lineCheck.passed) score += 15;
|
|
448
|
+
return Math.min(score, 100);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/core/installer.ts
|
|
452
|
+
async function installFromRegistry(name, version) {
|
|
453
|
+
const entry = await getSkillEntry(name);
|
|
454
|
+
if (!entry.repository) {
|
|
455
|
+
throw new InstallError(`Skill "${name}" has no repository URL`);
|
|
456
|
+
}
|
|
457
|
+
return installFromGithub(entry.repository, name);
|
|
458
|
+
}
|
|
459
|
+
async function installFromGithub(repoUrl, nameOverride) {
|
|
460
|
+
const name = nameOverride ?? repoUrl.split("/").pop()?.replace(/\.git$/, "") ?? "unknown";
|
|
461
|
+
const installPath = join3(SKILLS_DIR, name);
|
|
462
|
+
if (existsSync3(installPath)) {
|
|
463
|
+
await rm(installPath, { recursive: true });
|
|
464
|
+
}
|
|
465
|
+
await mkdir2(installPath, { recursive: true });
|
|
466
|
+
try {
|
|
467
|
+
execSync(`git clone --depth 1 "${repoUrl}" "${installPath}"`, {
|
|
468
|
+
stdio: "pipe",
|
|
469
|
+
timeout: 3e4
|
|
470
|
+
});
|
|
471
|
+
} catch {
|
|
472
|
+
throw new InstallError(`Failed to clone ${repoUrl}`);
|
|
473
|
+
}
|
|
474
|
+
const skillFile = join3(installPath, SKILL_FILENAME);
|
|
475
|
+
if (!existsSync3(skillFile)) {
|
|
476
|
+
await rm(installPath, { recursive: true });
|
|
477
|
+
throw new InstallError(`No ${SKILL_FILENAME} found in ${repoUrl}`);
|
|
478
|
+
}
|
|
479
|
+
const skill = await parseSkillFile(skillFile);
|
|
480
|
+
const safeguards = await runSafeguards(skill, installPath);
|
|
481
|
+
if (!safeguards.passed) {
|
|
482
|
+
await rm(installPath, { recursive: true });
|
|
483
|
+
const errors = safeguards.checks.filter((c) => !c.passed).map((c) => c.message);
|
|
484
|
+
throw new InstallError(`Skill failed safety checks: ${errors.join("; ")}`);
|
|
485
|
+
}
|
|
486
|
+
const installed = {
|
|
487
|
+
name: skill.metadata.name,
|
|
488
|
+
version: skill.metadata.version,
|
|
489
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
490
|
+
path: installPath,
|
|
491
|
+
source: "github"
|
|
492
|
+
};
|
|
493
|
+
await markInstalled(installed);
|
|
494
|
+
return installed;
|
|
495
|
+
}
|
|
496
|
+
async function installFromLocal(dirPath) {
|
|
497
|
+
const skillFile = join3(dirPath, SKILL_FILENAME);
|
|
498
|
+
if (!existsSync3(skillFile)) {
|
|
499
|
+
throw new InstallError(`No ${SKILL_FILENAME} found in ${dirPath}`);
|
|
500
|
+
}
|
|
501
|
+
const skill = await parseSkillFile(skillFile);
|
|
502
|
+
const safeguards = await runSafeguards(skill, dirPath);
|
|
503
|
+
if (!safeguards.passed) {
|
|
504
|
+
const errors = safeguards.checks.filter((c) => !c.passed).map((c) => c.message);
|
|
505
|
+
throw new InstallError(`Skill failed safety checks: ${errors.join("; ")}`);
|
|
506
|
+
}
|
|
507
|
+
const installPath = join3(SKILLS_DIR, skill.metadata.name);
|
|
508
|
+
if (existsSync3(installPath)) {
|
|
509
|
+
await rm(installPath, { recursive: true });
|
|
510
|
+
}
|
|
511
|
+
execSync(`cp -r "${dirPath}" "${installPath}"`, { stdio: "pipe" });
|
|
512
|
+
const installed = {
|
|
513
|
+
name: skill.metadata.name,
|
|
514
|
+
version: skill.metadata.version,
|
|
515
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
516
|
+
path: installPath,
|
|
517
|
+
source: "local"
|
|
518
|
+
};
|
|
519
|
+
await markInstalled(installed);
|
|
520
|
+
return installed;
|
|
521
|
+
}
|
|
522
|
+
async function uninstall(name) {
|
|
523
|
+
const installPath = join3(SKILLS_DIR, name);
|
|
524
|
+
if (existsSync3(installPath)) {
|
|
525
|
+
await rm(installPath, { recursive: true });
|
|
526
|
+
}
|
|
527
|
+
await markUninstalled(name);
|
|
528
|
+
}
|
|
529
|
+
async function linkToClaudeSkills(skill, projectDir = process.cwd()) {
|
|
530
|
+
const claudeSkillsDir = join3(projectDir, ".claude", "skills");
|
|
531
|
+
await mkdir2(claudeSkillsDir, { recursive: true });
|
|
532
|
+
const linkPath = join3(claudeSkillsDir, skill.name);
|
|
533
|
+
if (existsSync3(linkPath)) {
|
|
534
|
+
await rm(linkPath, { recursive: true });
|
|
535
|
+
}
|
|
536
|
+
await symlink(skill.path, linkPath, "dir");
|
|
537
|
+
return linkPath;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/core/ratings.ts
|
|
541
|
+
async function getRatings(name) {
|
|
542
|
+
const index = await getLocalIndex();
|
|
543
|
+
const entry = index.skills[name];
|
|
544
|
+
if (!entry) {
|
|
545
|
+
throw new SkillNotFoundError(name);
|
|
546
|
+
}
|
|
547
|
+
return entry.rating;
|
|
548
|
+
}
|
|
549
|
+
async function submitRating(name, rating, userId, comment) {
|
|
550
|
+
const index = await getLocalIndex();
|
|
551
|
+
const entry = index.skills[name];
|
|
552
|
+
if (!entry) {
|
|
553
|
+
throw new SkillNotFoundError(name);
|
|
554
|
+
}
|
|
555
|
+
const current = entry.rating;
|
|
556
|
+
const newCount = current.count + 1;
|
|
557
|
+
const newAverage = (current.average * current.count + rating) / newCount;
|
|
558
|
+
const dist = [...current.distribution];
|
|
559
|
+
dist[rating - 1] += 1;
|
|
560
|
+
entry.rating = {
|
|
561
|
+
average: Math.round(newAverage * 10) / 10,
|
|
562
|
+
count: newCount,
|
|
563
|
+
distribution: dist
|
|
564
|
+
};
|
|
565
|
+
await saveLocalIndex(index);
|
|
566
|
+
return entry.rating;
|
|
567
|
+
}
|
|
568
|
+
function formatRating(rating) {
|
|
569
|
+
const filled = Math.round(rating.average);
|
|
570
|
+
const empty = 5 - filled;
|
|
571
|
+
const stars = "\u2605".repeat(filled) + "\u2606".repeat(empty);
|
|
572
|
+
return `${stars} ${rating.average.toFixed(1)} (${rating.count} ratings)`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/core/publisher.ts
|
|
576
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
577
|
+
import { join as join4 } from "path";
|
|
578
|
+
import { createHash } from "crypto";
|
|
579
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, statSync } from "fs";
|
|
580
|
+
async function packageSkill(skillDir) {
|
|
581
|
+
const skillFile = join4(skillDir, SKILL_FILENAME);
|
|
582
|
+
if (!existsSync4(skillFile)) {
|
|
583
|
+
throw new SkillValidationError(`No ${SKILL_FILENAME} found in ${skillDir}`);
|
|
584
|
+
}
|
|
585
|
+
const skill = await parseSkillFile(skillFile);
|
|
586
|
+
const safeguards = await runSafeguards(skill, skillDir);
|
|
587
|
+
if (!safeguards.passed) {
|
|
588
|
+
const errors = safeguards.checks.filter((c) => !c.passed).map((c) => `[${c.severity}] ${c.message}`);
|
|
589
|
+
throw new SkillValidationError("Skill failed safety checks", errors);
|
|
590
|
+
}
|
|
591
|
+
const checksum = await computeDirectoryChecksum(skillDir);
|
|
592
|
+
const manifest = extractManifest(skill);
|
|
593
|
+
manifest.checksum = `sha256:${checksum}`;
|
|
594
|
+
manifest.files = listFiles(skillDir);
|
|
595
|
+
manifest.size_bytes = computeDirectorySize(skillDir);
|
|
596
|
+
return { skill, manifest, checksum };
|
|
597
|
+
}
|
|
598
|
+
async function computeDirectoryChecksum(dir) {
|
|
599
|
+
const hash = createHash("sha256");
|
|
600
|
+
const files = listFiles(dir).sort();
|
|
601
|
+
for (const file of files) {
|
|
602
|
+
const content = await readFile5(join4(dir, file));
|
|
603
|
+
hash.update(file);
|
|
604
|
+
hash.update(content);
|
|
605
|
+
}
|
|
606
|
+
return hash.digest("hex");
|
|
607
|
+
}
|
|
608
|
+
function listFiles(dir, prefix = "") {
|
|
609
|
+
const files = [];
|
|
610
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
611
|
+
for (const entry of entries) {
|
|
612
|
+
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
613
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
614
|
+
if (entry.isDirectory()) {
|
|
615
|
+
files.push(...listFiles(join4(dir, entry.name), relative));
|
|
616
|
+
} else {
|
|
617
|
+
files.push(relative);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return files;
|
|
621
|
+
}
|
|
622
|
+
function computeDirectorySize(dir) {
|
|
623
|
+
let size = 0;
|
|
624
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
625
|
+
for (const entry of entries) {
|
|
626
|
+
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
627
|
+
const fullPath = join4(dir, entry.name);
|
|
628
|
+
if (entry.isDirectory()) {
|
|
629
|
+
size += computeDirectorySize(fullPath);
|
|
630
|
+
} else {
|
|
631
|
+
size += statSync(fullPath).size;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return size;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/trawler/strategies.ts
|
|
638
|
+
async function searchRegistry(query) {
|
|
639
|
+
const index = await getLocalIndex();
|
|
640
|
+
const tokens = query.toLowerCase().split(/\s+/);
|
|
641
|
+
const results = [];
|
|
642
|
+
for (const entry of Object.values(index.skills)) {
|
|
643
|
+
const text = `${entry.name} ${entry.description} ${entry.tags.join(" ")}`.toLowerCase();
|
|
644
|
+
const matchCount = tokens.filter((t) => text.includes(t)).length;
|
|
645
|
+
if (matchCount === 0) continue;
|
|
646
|
+
results.push({
|
|
647
|
+
source: "registry",
|
|
648
|
+
skill: entry,
|
|
649
|
+
confidence: Math.min(matchCount / tokens.length, 1),
|
|
650
|
+
url: entry.repository || `skillli://registry/${entry.name}`
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
return results;
|
|
654
|
+
}
|
|
655
|
+
async function searchGithub(query) {
|
|
656
|
+
const results = [];
|
|
657
|
+
const searchQuery = encodeURIComponent(`${query} SKILL.md in:path`);
|
|
658
|
+
const url = `https://api.github.com/search/repositories?q=${searchQuery}&per_page=10`;
|
|
659
|
+
try {
|
|
660
|
+
const res = await fetch(url, {
|
|
661
|
+
headers: { Accept: "application/vnd.github.v3+json" }
|
|
662
|
+
});
|
|
663
|
+
if (!res.ok) return results;
|
|
664
|
+
const data = await res.json();
|
|
665
|
+
for (const repo of data.items ?? []) {
|
|
666
|
+
const confidence = Math.min(
|
|
667
|
+
0.3 + (repo.stargazers_count > 10 ? 0.2 : 0) + (repo.stargazers_count > 100 ? 0.2 : 0),
|
|
668
|
+
0.9
|
|
669
|
+
);
|
|
670
|
+
results.push({
|
|
671
|
+
source: "github",
|
|
672
|
+
skill: {
|
|
673
|
+
name: repo.full_name.split("/").pop() ?? repo.full_name,
|
|
674
|
+
description: repo.description ?? "",
|
|
675
|
+
author: repo.full_name.split("/")[0],
|
|
676
|
+
repository: repo.html_url,
|
|
677
|
+
tags: repo.topics ?? []
|
|
678
|
+
},
|
|
679
|
+
confidence,
|
|
680
|
+
url: repo.html_url
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
return results;
|
|
686
|
+
}
|
|
687
|
+
async function searchNpm(query) {
|
|
688
|
+
const results = [];
|
|
689
|
+
const searchQuery = encodeURIComponent(`${query} skill agent claude`);
|
|
690
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=${searchQuery}&size=10`;
|
|
691
|
+
try {
|
|
692
|
+
const res = await fetch(url);
|
|
693
|
+
if (!res.ok) return results;
|
|
694
|
+
const data = await res.json();
|
|
695
|
+
for (const obj of data.objects ?? []) {
|
|
696
|
+
const pkg = obj.package;
|
|
697
|
+
results.push({
|
|
698
|
+
source: "npm",
|
|
699
|
+
skill: {
|
|
700
|
+
name: pkg.name,
|
|
701
|
+
description: pkg.description ?? "",
|
|
702
|
+
version: pkg.version,
|
|
703
|
+
repository: pkg.links.repository ?? "",
|
|
704
|
+
tags: pkg.keywords ?? []
|
|
705
|
+
},
|
|
706
|
+
confidence: Math.min(obj.score.final * 0.7, 0.85),
|
|
707
|
+
url: pkg.links.npm
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
}
|
|
712
|
+
return results;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/trawler/ranker.ts
|
|
716
|
+
function deduplicateResults(results) {
|
|
717
|
+
const seen = /* @__PURE__ */ new Map();
|
|
718
|
+
for (const result of results) {
|
|
719
|
+
const key = result.skill.name?.toLowerCase() ?? result.url;
|
|
720
|
+
const existing = seen.get(key);
|
|
721
|
+
if (!existing || result.confidence > existing.confidence) {
|
|
722
|
+
seen.set(key, result);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return Array.from(seen.values());
|
|
726
|
+
}
|
|
727
|
+
function rankResults(results, query) {
|
|
728
|
+
const tokens = query.toLowerCase().split(/\s+/);
|
|
729
|
+
return results.map((result) => {
|
|
730
|
+
let bonus = 0;
|
|
731
|
+
if (result.source === "registry") bonus += 0.2;
|
|
732
|
+
else if (result.source === "github") bonus += 0.05;
|
|
733
|
+
const name = (result.skill.name ?? "").toLowerCase();
|
|
734
|
+
for (const token of tokens) {
|
|
735
|
+
if (name.includes(token)) bonus += 0.15;
|
|
736
|
+
}
|
|
737
|
+
const tags = (result.skill.tags ?? []).map((t) => t.toLowerCase());
|
|
738
|
+
for (const token of tokens) {
|
|
739
|
+
if (tags.includes(token)) bonus += 0.1;
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
...result,
|
|
743
|
+
confidence: Math.min(result.confidence + bonus, 1)
|
|
744
|
+
};
|
|
745
|
+
}).sort((a, b) => b.confidence - a.confidence);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/trawler/index.ts
|
|
749
|
+
async function trawl(query, options = {}) {
|
|
750
|
+
const sources = options.sources ?? ["registry", "github"];
|
|
751
|
+
const maxResults = options.maxResults ?? 10;
|
|
752
|
+
const searches = [];
|
|
753
|
+
if (sources.includes("registry")) {
|
|
754
|
+
searches.push(searchRegistry(query));
|
|
755
|
+
}
|
|
756
|
+
if (sources.includes("github")) {
|
|
757
|
+
searches.push(searchGithub(query));
|
|
758
|
+
}
|
|
759
|
+
if (sources.includes("npm")) {
|
|
760
|
+
searches.push(searchNpm(query));
|
|
761
|
+
}
|
|
762
|
+
const allResults = (await Promise.all(searches)).flat();
|
|
763
|
+
const deduplicated = deduplicateResults(allResults);
|
|
764
|
+
const ranked = rankResults(deduplicated, query);
|
|
765
|
+
return ranked.slice(0, maxResults);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/mcp/server.ts
|
|
769
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
770
|
+
import { z as z2 } from "zod";
|
|
771
|
+
function createSkillliMcpServer() {
|
|
772
|
+
const server = new McpServer({
|
|
773
|
+
name: "skillli",
|
|
774
|
+
version: "0.1.0"
|
|
775
|
+
});
|
|
776
|
+
server.tool(
|
|
777
|
+
"search_skills",
|
|
778
|
+
"Search the skillli registry for agentic AI skills",
|
|
779
|
+
{
|
|
780
|
+
query: z2.string().describe("Search query"),
|
|
781
|
+
tags: z2.array(z2.string()).optional().describe("Filter by tags"),
|
|
782
|
+
category: z2.string().optional().describe("Filter by category"),
|
|
783
|
+
limit: z2.number().optional().default(10).describe("Max results")
|
|
784
|
+
},
|
|
785
|
+
async ({ query, tags, category, limit }) => {
|
|
786
|
+
const index = await getLocalIndex();
|
|
787
|
+
const results = search(index, {
|
|
788
|
+
query,
|
|
789
|
+
tags,
|
|
790
|
+
category,
|
|
791
|
+
limit
|
|
792
|
+
});
|
|
793
|
+
const text = results.length === 0 ? "No skills found matching your query." : results.map((r) => {
|
|
794
|
+
const s = r.skill;
|
|
795
|
+
return `**${s.name}** v${s.version} [${s.trustLevel.toUpperCase()}]
|
|
796
|
+
${s.description}
|
|
797
|
+
Rating: ${s.rating.average}/5 (${s.rating.count}) | Downloads: ${s.downloads}
|
|
798
|
+
Tags: ${s.tags.join(", ")}`;
|
|
799
|
+
}).join("\n\n");
|
|
800
|
+
return { content: [{ type: "text", text }] };
|
|
801
|
+
}
|
|
802
|
+
);
|
|
803
|
+
server.tool(
|
|
804
|
+
"install_skill",
|
|
805
|
+
"Install an agentic skill from the skillli registry",
|
|
806
|
+
{
|
|
807
|
+
name: z2.string().describe("Skill name to install"),
|
|
808
|
+
link: z2.boolean().optional().default(true).describe("Link to .claude/skills/")
|
|
809
|
+
},
|
|
810
|
+
async ({ name, link }) => {
|
|
811
|
+
try {
|
|
812
|
+
const installed = await installFromRegistry(name);
|
|
813
|
+
let text = `Installed ${installed.name} v${installed.version} at ${installed.path}`;
|
|
814
|
+
if (link) {
|
|
815
|
+
const linkPath = await linkToClaudeSkills(installed);
|
|
816
|
+
text += `
|
|
817
|
+
Linked to ${linkPath}`;
|
|
818
|
+
}
|
|
819
|
+
return { content: [{ type: "text", text }] };
|
|
820
|
+
} catch (error) {
|
|
821
|
+
return { content: [{ type: "text", text: `Install failed: ${error}` }], isError: true };
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
);
|
|
825
|
+
server.tool(
|
|
826
|
+
"get_skill_info",
|
|
827
|
+
"Get detailed information about a skill including trust score",
|
|
828
|
+
{
|
|
829
|
+
name: z2.string().describe("Skill name")
|
|
830
|
+
},
|
|
831
|
+
async ({ name }) => {
|
|
832
|
+
try {
|
|
833
|
+
const entry = await getSkillEntry(name);
|
|
834
|
+
const text = [
|
|
835
|
+
`**${entry.name}** v${entry.version} [${entry.trustLevel.toUpperCase()}]`,
|
|
836
|
+
`${entry.description}`,
|
|
837
|
+
`Author: ${entry.author}`,
|
|
838
|
+
`Category: ${entry.category}`,
|
|
839
|
+
`Tags: ${entry.tags.join(", ")}`,
|
|
840
|
+
`Rating: ${entry.rating.average}/5 (${entry.rating.count} ratings)`,
|
|
841
|
+
`Downloads: ${entry.downloads}`,
|
|
842
|
+
entry.repository ? `Repository: ${entry.repository}` : null,
|
|
843
|
+
`Published: ${entry.publishedAt}`,
|
|
844
|
+
`Updated: ${entry.updatedAt}`
|
|
845
|
+
].filter(Boolean).join("\n");
|
|
846
|
+
return { content: [{ type: "text", text }] };
|
|
847
|
+
} catch (error) {
|
|
848
|
+
return { content: [{ type: "text", text: `Error: ${error}` }], isError: true };
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
);
|
|
852
|
+
server.tool(
|
|
853
|
+
"trawl_skills",
|
|
854
|
+
"Agentic search across multiple sources (registry, GitHub, npm) for skills matching a need",
|
|
855
|
+
{
|
|
856
|
+
query: z2.string().describe("What kind of skill you need"),
|
|
857
|
+
sources: z2.array(z2.enum(["registry", "github", "npm"])).optional().default(["registry", "github"]),
|
|
858
|
+
maxResults: z2.number().optional().default(5)
|
|
859
|
+
},
|
|
860
|
+
async ({ query, sources, maxResults }) => {
|
|
861
|
+
const results = await trawl(query, { sources, maxResults });
|
|
862
|
+
if (results.length === 0) {
|
|
863
|
+
return { content: [{ type: "text", text: "No skills found across any source." }] };
|
|
864
|
+
}
|
|
865
|
+
const text = results.map((r) => {
|
|
866
|
+
const conf = Math.round(r.confidence * 100);
|
|
867
|
+
return `**${r.skill.name ?? "unknown"}** [${r.source}] (${conf}% match)
|
|
868
|
+
${r.skill.description ?? ""}
|
|
869
|
+
${r.url}`;
|
|
870
|
+
}).join("\n\n");
|
|
871
|
+
return { content: [{ type: "text", text }] };
|
|
872
|
+
}
|
|
873
|
+
);
|
|
874
|
+
server.tool(
|
|
875
|
+
"rate_skill",
|
|
876
|
+
"Rate an installed skill (1-5 stars)",
|
|
877
|
+
{
|
|
878
|
+
name: z2.string().describe("Skill name to rate"),
|
|
879
|
+
rating: z2.number().min(1).max(5).describe("Rating 1-5"),
|
|
880
|
+
comment: z2.string().optional().describe("Optional review comment")
|
|
881
|
+
},
|
|
882
|
+
async ({ name, rating, comment }) => {
|
|
883
|
+
try {
|
|
884
|
+
const updated = await submitRating(name, rating, "mcp-user", comment);
|
|
885
|
+
return {
|
|
886
|
+
content: [
|
|
887
|
+
{ type: "text", text: `Rated ${name}: ${formatRating(updated)}` }
|
|
888
|
+
]
|
|
889
|
+
};
|
|
890
|
+
} catch (error) {
|
|
891
|
+
return { content: [{ type: "text", text: `Rating failed: ${error}` }], isError: true };
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
);
|
|
895
|
+
server.resource(
|
|
896
|
+
"installed-skills",
|
|
897
|
+
"skillli://installed",
|
|
898
|
+
{ description: "List of all currently installed skillli skills" },
|
|
899
|
+
async () => {
|
|
900
|
+
const skills = await getInstalledSkills();
|
|
901
|
+
return {
|
|
902
|
+
contents: [
|
|
903
|
+
{
|
|
904
|
+
uri: "skillli://installed",
|
|
905
|
+
mimeType: "application/json",
|
|
906
|
+
text: JSON.stringify(skills, null, 2)
|
|
907
|
+
}
|
|
908
|
+
]
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
);
|
|
912
|
+
server.resource(
|
|
913
|
+
"skill-index",
|
|
914
|
+
"skillli://index",
|
|
915
|
+
{ description: "The full skillli registry index" },
|
|
916
|
+
async () => {
|
|
917
|
+
const index = await getLocalIndex();
|
|
918
|
+
return {
|
|
919
|
+
contents: [
|
|
920
|
+
{
|
|
921
|
+
uri: "skillli://index",
|
|
922
|
+
mimeType: "application/json",
|
|
923
|
+
text: JSON.stringify(index, null, 2)
|
|
924
|
+
}
|
|
925
|
+
]
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
);
|
|
929
|
+
return server;
|
|
930
|
+
}
|
|
931
|
+
export {
|
|
932
|
+
SkillMetadataSchema,
|
|
933
|
+
VERSION,
|
|
934
|
+
computeTrustScore,
|
|
935
|
+
createSkillliMcpServer,
|
|
936
|
+
extractManifest,
|
|
937
|
+
fetchIndex,
|
|
938
|
+
formatRating,
|
|
939
|
+
getConfig,
|
|
940
|
+
getInstalledSkills,
|
|
941
|
+
getLocalIndex,
|
|
942
|
+
getRatings,
|
|
943
|
+
getSkillEntry,
|
|
944
|
+
installFromGithub,
|
|
945
|
+
installFromLocal,
|
|
946
|
+
installFromRegistry,
|
|
947
|
+
linkToClaudeSkills,
|
|
948
|
+
packageSkill,
|
|
949
|
+
parseSkillContent,
|
|
950
|
+
parseSkillFile,
|
|
951
|
+
runSafeguards,
|
|
952
|
+
search,
|
|
953
|
+
searchByCategory,
|
|
954
|
+
searchByTags,
|
|
955
|
+
submitRating,
|
|
956
|
+
syncIndex,
|
|
957
|
+
trawl,
|
|
958
|
+
uninstall,
|
|
959
|
+
validateMetadata
|
|
960
|
+
};
|
|
961
|
+
//# sourceMappingURL=index.js.map
|