skybridge 0.0.0-dev.b6ba410 → 0.0.0-dev.b6c9d23
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/dist/cli/build-helpers.d.ts +1 -2
- package/dist/cli/build-helpers.js +6 -17
- package/dist/cli/build-helpers.js.map +1 -1
- package/dist/cli/build-helpers.test.js +3 -33
- package/dist/cli/build-helpers.test.js.map +1 -1
- package/dist/cli/build-steps.js +1 -6
- package/dist/cli/build-steps.js.map +1 -1
- package/dist/cli/use-execute-steps.d.ts +1 -1
- package/dist/server/auth/providers/custom.d.ts +6 -0
- package/dist/server/auth/providers/custom.js +7 -4
- package/dist/server/auth/providers/custom.js.map +1 -1
- package/dist/server/auth/providers/custom.test.js +21 -0
- package/dist/server/auth/providers/custom.test.js.map +1 -1
- package/dist/server/auth/providers/descope.js +4 -2
- package/dist/server/auth/providers/descope.js.map +1 -1
- package/dist/server/auth/providers/descope.test.js +12 -10
- package/dist/server/auth/providers/descope.test.js.map +1 -1
- package/dist/server/index.d.ts +1 -2
- package/dist/server/index.js +1 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/server.d.ts +0 -9
- package/dist/server/server.js +2 -48
- package/dist/server/server.js.map +1 -1
- package/dist/web/bridges/adaptor.js +20 -0
- package/dist/web/bridges/adaptor.js.map +1 -1
- package/dist/web/bridges/adaptor.test.js +11 -0
- package/dist/web/bridges/adaptor.test.js.map +1 -1
- package/dist/web/data-llm.test.js +8 -0
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/plugin/plugin.js +0 -1
- package/dist/web/plugin/plugin.js.map +1 -1
- package/package.json +8 -8
- package/dist/server/skills-integration.test.d.ts +0 -1
- package/dist/server/skills-integration.test.js +0 -63
- package/dist/server/skills-integration.test.js.map +0 -1
- package/dist/server/skills.d.ts +0 -47
- package/dist/server/skills.js +0 -305
- package/dist/server/skills.js.map +0 -1
- package/dist/server/skills.test.d.ts +0 -1
- package/dist/server/skills.test.js +0 -214
- package/dist/server/skills.test.js.map +0 -1
package/dist/server/skills.js
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, realpathSync, statSync, } from "node:fs";
|
|
3
|
-
import { extname, join, resolve, sep } from "node:path";
|
|
4
|
-
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
-
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
/**
|
|
8
|
-
* Skills over MCP — the Skills Extension ([SEP-2640](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640), experimental).
|
|
9
|
-
* All spec-facing logic is kept in this one module so churn in the in-review
|
|
10
|
-
* SEP stays a single-file change.
|
|
11
|
-
*/
|
|
12
|
-
export const SKILLS_EXTENSION_KEY = "io.modelcontextprotocol/skills";
|
|
13
|
-
export const SKILL_INDEX_URI = "skill://index.json";
|
|
14
|
-
const SKILL_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
15
|
-
// Only the fields the extension depends on are validated; `.loose()` keeps any
|
|
16
|
-
// other frontmatter, which the index echoes verbatim.
|
|
17
|
-
const SkillFrontmatterSchema = z
|
|
18
|
-
.object({
|
|
19
|
-
name: z
|
|
20
|
-
.string()
|
|
21
|
-
.min(1)
|
|
22
|
-
.max(64)
|
|
23
|
-
.regex(SKILL_NAME_RE, "must be lowercase alphanumeric words separated by single hyphens"),
|
|
24
|
-
description: z.string().min(1).max(1024),
|
|
25
|
-
})
|
|
26
|
-
.loose();
|
|
27
|
-
const mimeTypeForFile = (name) => {
|
|
28
|
-
switch (extname(name)) {
|
|
29
|
-
case ".md":
|
|
30
|
-
return "text/markdown";
|
|
31
|
-
case ".json":
|
|
32
|
-
return "application/json";
|
|
33
|
-
default:
|
|
34
|
-
return "text/plain";
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
const sha256 = (content) => `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
|
|
38
|
-
const unquote = (raw) => {
|
|
39
|
-
const v = raw.trim();
|
|
40
|
-
if (v.length >= 2 &&
|
|
41
|
-
((v.startsWith('"') && v.endsWith('"')) ||
|
|
42
|
-
(v.startsWith("'") && v.endsWith("'")))) {
|
|
43
|
-
return v.slice(1, -1);
|
|
44
|
-
}
|
|
45
|
-
return v;
|
|
46
|
-
};
|
|
47
|
-
// Supports only the frontmatter subset the Agent Skills spec uses — top-level
|
|
48
|
-
// scalars plus one level of nested `key: value` maps. Anything else (sequences,
|
|
49
|
-
// block scalars, deeper nesting) throws rather than silently mis-parsing, which
|
|
50
|
-
// avoids pulling in a full YAML dependency.
|
|
51
|
-
function parseFrontmatter(content, source) {
|
|
52
|
-
const match = /^?\s*---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(content);
|
|
53
|
-
if (!match) {
|
|
54
|
-
throw new Error(`Skill ${source} is missing YAML frontmatter`);
|
|
55
|
-
}
|
|
56
|
-
const result = {};
|
|
57
|
-
let currentMap = null;
|
|
58
|
-
for (const line of (match[1] ?? "").split(/\r?\n/)) {
|
|
59
|
-
if (line.trim() === "" || line.trim().startsWith("#")) {
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (/^\s/.test(line)) {
|
|
63
|
-
const nested = /^\s+([^:#]+):[ \t]?(.*)$/.exec(line);
|
|
64
|
-
const nestedKey = nested?.[1];
|
|
65
|
-
if (!currentMap || nestedKey === undefined) {
|
|
66
|
-
throw new Error(`Cannot parse frontmatter of ${source} at: "${line}"`);
|
|
67
|
-
}
|
|
68
|
-
currentMap[nestedKey.trim()] = unquote(nested?.[2] ?? "");
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
const top = /^([^:#\s][^:]*):[ \t]?(.*)$/.exec(line);
|
|
72
|
-
const topKey = top?.[1];
|
|
73
|
-
if (topKey === undefined) {
|
|
74
|
-
throw new Error(`Cannot parse frontmatter of ${source} at: "${line}"`);
|
|
75
|
-
}
|
|
76
|
-
const key = topKey.trim();
|
|
77
|
-
const value = top?.[2] ?? "";
|
|
78
|
-
if (value.trim() === "") {
|
|
79
|
-
currentMap = {};
|
|
80
|
-
result[key] = currentMap;
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
if (/^[-|>[{]/.test(value.trim())) {
|
|
84
|
-
throw new Error(`Unsupported frontmatter value for "${key}" in ${source}: only scalars and simple maps are supported`);
|
|
85
|
-
}
|
|
86
|
-
result[key] = unquote(value);
|
|
87
|
-
currentMap = null;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return result;
|
|
91
|
-
}
|
|
92
|
-
const readSkillDir = (root, rel = "") => {
|
|
93
|
-
const files = {};
|
|
94
|
-
for (const entry of readdirSync(join(root, rel), { withFileTypes: true })) {
|
|
95
|
-
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
96
|
-
if (entry.isDirectory()) {
|
|
97
|
-
Object.assign(files, readSkillDir(root, childRel));
|
|
98
|
-
}
|
|
99
|
-
else if (entry.isFile()) {
|
|
100
|
-
files[childRel] = {
|
|
101
|
-
text: readFileSync(join(root, childRel), "utf8"),
|
|
102
|
-
mimeType: mimeTypeForFile(entry.name),
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return files;
|
|
107
|
-
};
|
|
108
|
-
// Each immediate subdirectory with a `SKILL.md` is one skill; its directory name
|
|
109
|
-
// must equal the frontmatter `name`. Throws on any invalid skill so the
|
|
110
|
-
// build/dev startup fails loudly instead of silently skipping.
|
|
111
|
-
export function discoverSkills(dir) {
|
|
112
|
-
if (!existsSync(dir)) {
|
|
113
|
-
return [];
|
|
114
|
-
}
|
|
115
|
-
const skills = [];
|
|
116
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
117
|
-
if (!entry.isDirectory()) {
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
const skillRoot = join(dir, entry.name);
|
|
121
|
-
const skillMdPath = join(skillRoot, "SKILL.md");
|
|
122
|
-
if (!existsSync(skillMdPath)) {
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
const skillMd = readFileSync(skillMdPath, "utf8");
|
|
126
|
-
const source = `${entry.name}/SKILL.md`;
|
|
127
|
-
const raw = parseFrontmatter(skillMd, source);
|
|
128
|
-
const parsed = SkillFrontmatterSchema.safeParse(raw);
|
|
129
|
-
if (!parsed.success) {
|
|
130
|
-
throw new Error(`Invalid skill frontmatter in ${source}: ${z.prettifyError(parsed.error)}`);
|
|
131
|
-
}
|
|
132
|
-
if (parsed.data.name !== entry.name) {
|
|
133
|
-
throw new Error(`Skill name "${parsed.data.name}" in ${source} must match its directory name "${entry.name}"`);
|
|
134
|
-
}
|
|
135
|
-
skills.push({
|
|
136
|
-
name: entry.name,
|
|
137
|
-
frontmatter: raw,
|
|
138
|
-
digest: sha256(skillMd),
|
|
139
|
-
files: readSkillDir(skillRoot),
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
return skills;
|
|
143
|
-
}
|
|
144
|
-
// Parses a `skill://<name>/<subpath>` URI and rejects `..`/`.` traversal.
|
|
145
|
-
export function skillUriToRelPath(uri) {
|
|
146
|
-
const match = /^skill:\/\/([^/]+)(?:\/(.*))?$/.exec(uri.replace(/\/+$/, ""));
|
|
147
|
-
if (!match) {
|
|
148
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);
|
|
149
|
-
}
|
|
150
|
-
const [, name = "", relPath = ""] = match;
|
|
151
|
-
const segments = relPath.split("/").filter(Boolean);
|
|
152
|
-
if (name === "" || segments.includes("..") || segments.includes(".")) {
|
|
153
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);
|
|
154
|
-
}
|
|
155
|
-
return { name, relPath: segments.join("/") };
|
|
156
|
-
}
|
|
157
|
-
export function manifestSource(manifest) {
|
|
158
|
-
const byName = new Map(manifest.map((s) => [s.name, s]));
|
|
159
|
-
const fileKey = (relPath) => relPath.replace(/\/+$/, "");
|
|
160
|
-
return {
|
|
161
|
-
list: () => manifest,
|
|
162
|
-
readFile: (name, relPath) => byName.get(name)?.files[relPath] ?? null,
|
|
163
|
-
readDir: (name, relPath) => {
|
|
164
|
-
const skill = byName.get(name);
|
|
165
|
-
if (!skill) {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
const prefix = relPath === "" ? "" : `${fileKey(relPath)}/`;
|
|
169
|
-
const children = new Map();
|
|
170
|
-
let matched = relPath === "";
|
|
171
|
-
for (const filePath of Object.keys(skill.files)) {
|
|
172
|
-
if (!filePath.startsWith(prefix)) {
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
matched = true;
|
|
176
|
-
const rest = filePath.slice(prefix.length);
|
|
177
|
-
const slash = rest.indexOf("/");
|
|
178
|
-
if (slash === -1) {
|
|
179
|
-
children.set(rest, mimeTypeForFile(rest));
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
children.set(rest.slice(0, slash), "inode/directory");
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (!matched) {
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
return [...children].map(([name, mimeType]) => ({ name, mimeType }));
|
|
189
|
-
},
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
export function diskSource(dir) {
|
|
193
|
-
const rootReal = existsSync(dir) ? realpathSync(dir) : resolve(dir);
|
|
194
|
-
// Confirms the real (symlink-followed) path stays within the skills root, else
|
|
195
|
-
// null — closes the traversal hole where an in-tree symlink points outside it.
|
|
196
|
-
const contain = (name, relPath) => {
|
|
197
|
-
const path = join(dir, name, relPath);
|
|
198
|
-
if (!existsSync(path)) {
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
const real = realpathSync(path);
|
|
202
|
-
return real === rootReal || real.startsWith(`${rootReal}${sep}`)
|
|
203
|
-
? path
|
|
204
|
-
: null;
|
|
205
|
-
};
|
|
206
|
-
return {
|
|
207
|
-
list: () => discoverSkills(dir),
|
|
208
|
-
readFile: (name, relPath) => {
|
|
209
|
-
const path = contain(name, relPath || "SKILL.md");
|
|
210
|
-
if (!path || !statSync(path).isFile()) {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
text: readFileSync(path, "utf8"),
|
|
215
|
-
mimeType: mimeTypeForFile(path),
|
|
216
|
-
};
|
|
217
|
-
},
|
|
218
|
-
readDir: (name, relPath) => {
|
|
219
|
-
const path = contain(name, relPath);
|
|
220
|
-
if (!path || !statSync(path).isDirectory()) {
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
return readdirSync(path, { withFileTypes: true }).map((entry) => ({
|
|
224
|
-
name: entry.name,
|
|
225
|
-
mimeType: entry.isDirectory()
|
|
226
|
-
? "inode/directory"
|
|
227
|
-
: mimeTypeForFile(entry.name),
|
|
228
|
-
}));
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
const DirectoryReadRequestSchema = z.object({
|
|
233
|
-
method: z.literal("resources/directory/read"),
|
|
234
|
-
params: z.object({ uri: z.string(), cursor: z.string().optional() }),
|
|
235
|
-
});
|
|
236
|
-
// Registers per SEP-2640: one resource per `SKILL.md`, a template for supporting
|
|
237
|
-
// files, the `skill://index.json` index, and optionally `resources/directory/read`.
|
|
238
|
-
export function registerSkills(server, source, { directoryRead }) {
|
|
239
|
-
for (const skill of source.list()) {
|
|
240
|
-
const uri = `skill://${skill.name}/SKILL.md`;
|
|
241
|
-
server.registerResource(skill.name, uri, {
|
|
242
|
-
description: typeof skill.frontmatter.description === "string"
|
|
243
|
-
? skill.frontmatter.description
|
|
244
|
-
: undefined,
|
|
245
|
-
mimeType: "text/markdown",
|
|
246
|
-
}, (readUri) => {
|
|
247
|
-
const file = source.readFile(skill.name, "SKILL.md");
|
|
248
|
-
if (!file) {
|
|
249
|
-
throw new McpError(ErrorCode.InvalidParams, `Not found: ${readUri.href}`);
|
|
250
|
-
}
|
|
251
|
-
return {
|
|
252
|
-
contents: [
|
|
253
|
-
{ uri: readUri.href, text: file.text, mimeType: file.mimeType },
|
|
254
|
-
],
|
|
255
|
-
};
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
server.registerResource("skill-files", new ResourceTemplate("skill://{skillName}/{+filePath}", {
|
|
259
|
-
list: undefined,
|
|
260
|
-
}), {}, (readUri) => {
|
|
261
|
-
const { name, relPath } = skillUriToRelPath(readUri.href);
|
|
262
|
-
const file = source.readFile(name, relPath);
|
|
263
|
-
if (!file) {
|
|
264
|
-
throw new McpError(ErrorCode.InvalidParams, `Not found: ${readUri.href}`);
|
|
265
|
-
}
|
|
266
|
-
return {
|
|
267
|
-
contents: [
|
|
268
|
-
{ uri: readUri.href, text: file.text, mimeType: file.mimeType },
|
|
269
|
-
],
|
|
270
|
-
};
|
|
271
|
-
});
|
|
272
|
-
server.registerResource("skill-index", SKILL_INDEX_URI, { mimeType: "application/json" }, () => ({
|
|
273
|
-
contents: [
|
|
274
|
-
{
|
|
275
|
-
uri: SKILL_INDEX_URI,
|
|
276
|
-
mimeType: "application/json",
|
|
277
|
-
text: JSON.stringify({
|
|
278
|
-
skills: source.list().map((skill) => ({
|
|
279
|
-
url: `skill://${skill.name}/SKILL.md`,
|
|
280
|
-
digest: skill.digest,
|
|
281
|
-
frontmatter: skill.frontmatter,
|
|
282
|
-
})),
|
|
283
|
-
}),
|
|
284
|
-
},
|
|
285
|
-
],
|
|
286
|
-
}));
|
|
287
|
-
if (directoryRead) {
|
|
288
|
-
server.server.setRequestHandler(DirectoryReadRequestSchema, ({ params }) => {
|
|
289
|
-
const { name, relPath } = skillUriToRelPath(params.uri);
|
|
290
|
-
const entries = source.readDir(name, relPath);
|
|
291
|
-
if (!entries) {
|
|
292
|
-
throw new McpError(ErrorCode.InvalidParams, `Not a directory: ${params.uri}`);
|
|
293
|
-
}
|
|
294
|
-
const base = params.uri.replace(/\/+$/, "");
|
|
295
|
-
return {
|
|
296
|
-
resources: entries.map((entry) => ({
|
|
297
|
-
uri: `${base}/${entry.name}`,
|
|
298
|
-
name: entry.name,
|
|
299
|
-
mimeType: entry.mimeType,
|
|
300
|
-
})),
|
|
301
|
-
};
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
//# sourceMappingURL=skills.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"skills.js","sourceRoot":"","sources":["../../src/server/skills.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport {\n existsSync,\n readdirSync,\n readFileSync,\n realpathSync,\n statSync,\n} from \"node:fs\";\nimport { extname, join, resolve, sep } from \"node:path\";\nimport type {\n ReadResourceCallback,\n ReadResourceTemplateCallback,\n ResourceMetadata,\n McpServer as SdkMcpServer,\n} from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { ResourceTemplate } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { ErrorCode, McpError } from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\n\n/**\n * Skills over MCP — the Skills Extension ([SEP-2640](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640), experimental).\n * All spec-facing logic is kept in this one module so churn in the in-review\n * SEP stays a single-file change.\n */\n\nexport const SKILLS_EXTENSION_KEY = \"io.modelcontextprotocol/skills\";\nexport const SKILL_INDEX_URI = \"skill://index.json\";\n\nconst SKILL_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\n// Only the fields the extension depends on are validated; `.loose()` keeps any\n// other frontmatter, which the index echoes verbatim.\nconst SkillFrontmatterSchema = z\n .object({\n name: z\n .string()\n .min(1)\n .max(64)\n .regex(\n SKILL_NAME_RE,\n \"must be lowercase alphanumeric words separated by single hyphens\",\n ),\n description: z.string().min(1).max(1024),\n })\n .loose();\n\nexport interface SkillFile {\n text: string;\n mimeType: string;\n}\n\nexport interface Skill {\n name: string;\n frontmatter: Record<string, unknown>;\n /** `sha256:<hex>` over the raw `SKILL.md` bytes. */\n digest: string;\n /** Keyed by POSIX path relative to the skill directory, including `SKILL.md`. */\n files: Record<string, SkillFile>;\n}\n\nexport type SkillsManifest = Skill[];\n\nconst mimeTypeForFile = (name: string): string => {\n switch (extname(name)) {\n case \".md\":\n return \"text/markdown\";\n case \".json\":\n return \"application/json\";\n default:\n return \"text/plain\";\n }\n};\n\nconst sha256 = (content: string): string =>\n `sha256:${createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\")}`;\n\nconst unquote = (raw: string): string => {\n const v = raw.trim();\n if (\n v.length >= 2 &&\n ((v.startsWith('\"') && v.endsWith('\"')) ||\n (v.startsWith(\"'\") && v.endsWith(\"'\")))\n ) {\n return v.slice(1, -1);\n }\n return v;\n};\n\n// Supports only the frontmatter subset the Agent Skills spec uses — top-level\n// scalars plus one level of nested `key: value` maps. Anything else (sequences,\n// block scalars, deeper nesting) throws rather than silently mis-parsing, which\n// avoids pulling in a full YAML dependency.\nfunction parseFrontmatter(\n content: string,\n source: string,\n): Record<string, unknown> {\n const match = /^?\\s*---[ \\t]*\\r?\\n([\\s\\S]*?)\\r?\\n---[ \\t]*(?:\\r?\\n|$)/.exec(\n content,\n );\n if (!match) {\n throw new Error(`Skill ${source} is missing YAML frontmatter`);\n }\n\n const result: Record<string, unknown> = {};\n let currentMap: Record<string, string> | null = null;\n\n for (const line of (match[1] ?? \"\").split(/\\r?\\n/)) {\n if (line.trim() === \"\" || line.trim().startsWith(\"#\")) {\n continue;\n }\n\n if (/^\\s/.test(line)) {\n const nested = /^\\s+([^:#]+):[ \\t]?(.*)$/.exec(line);\n const nestedKey = nested?.[1];\n if (!currentMap || nestedKey === undefined) {\n throw new Error(`Cannot parse frontmatter of ${source} at: \"${line}\"`);\n }\n currentMap[nestedKey.trim()] = unquote(nested?.[2] ?? \"\");\n continue;\n }\n\n const top = /^([^:#\\s][^:]*):[ \\t]?(.*)$/.exec(line);\n const topKey = top?.[1];\n if (topKey === undefined) {\n throw new Error(`Cannot parse frontmatter of ${source} at: \"${line}\"`);\n }\n const key = topKey.trim();\n const value = top?.[2] ?? \"\";\n if (value.trim() === \"\") {\n currentMap = {};\n result[key] = currentMap;\n } else {\n if (/^[-|>[{]/.test(value.trim())) {\n throw new Error(\n `Unsupported frontmatter value for \"${key}\" in ${source}: only scalars and simple maps are supported`,\n );\n }\n result[key] = unquote(value);\n currentMap = null;\n }\n }\n return result;\n}\n\nconst readSkillDir = (root: string, rel = \"\"): Record<string, SkillFile> => {\n const files: Record<string, SkillFile> = {};\n for (const entry of readdirSync(join(root, rel), { withFileTypes: true })) {\n const childRel = rel ? `${rel}/${entry.name}` : entry.name;\n if (entry.isDirectory()) {\n Object.assign(files, readSkillDir(root, childRel));\n } else if (entry.isFile()) {\n files[childRel] = {\n text: readFileSync(join(root, childRel), \"utf8\"),\n mimeType: mimeTypeForFile(entry.name),\n };\n }\n }\n return files;\n};\n\n// Each immediate subdirectory with a `SKILL.md` is one skill; its directory name\n// must equal the frontmatter `name`. Throws on any invalid skill so the\n// build/dev startup fails loudly instead of silently skipping.\nexport function discoverSkills(dir: string): SkillsManifest {\n if (!existsSync(dir)) {\n return [];\n }\n\n const skills: SkillsManifest = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (!entry.isDirectory()) {\n continue;\n }\n\n const skillRoot = join(dir, entry.name);\n const skillMdPath = join(skillRoot, \"SKILL.md\");\n if (!existsSync(skillMdPath)) {\n continue;\n }\n\n const skillMd = readFileSync(skillMdPath, \"utf8\");\n const source = `${entry.name}/SKILL.md`;\n const raw = parseFrontmatter(skillMd, source);\n\n const parsed = SkillFrontmatterSchema.safeParse(raw);\n if (!parsed.success) {\n throw new Error(\n `Invalid skill frontmatter in ${source}: ${z.prettifyError(parsed.error)}`,\n );\n }\n if (parsed.data.name !== entry.name) {\n throw new Error(\n `Skill name \"${parsed.data.name}\" in ${source} must match its directory name \"${entry.name}\"`,\n );\n }\n\n skills.push({\n name: entry.name,\n frontmatter: raw,\n digest: sha256(skillMd),\n files: readSkillDir(skillRoot),\n });\n }\n return skills;\n}\n\n// Parses a `skill://<name>/<subpath>` URI and rejects `..`/`.` traversal.\nexport function skillUriToRelPath(uri: string): {\n name: string;\n relPath: string;\n} {\n const match = /^skill:\\/\\/([^/]+)(?:\\/(.*))?$/.exec(uri.replace(/\\/+$/, \"\"));\n if (!match) {\n throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);\n }\n const [, name = \"\", relPath = \"\"] = match;\n const segments = relPath.split(\"/\").filter(Boolean);\n if (name === \"\" || segments.includes(\"..\") || segments.includes(\".\")) {\n throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);\n }\n return { name, relPath: segments.join(\"/\") };\n}\n\n// The indirection both runtime modes share: an in-memory manifest (production)\n// and a live disk reader (dev). `readFile`/`readDir` return null when absent.\nexport interface SkillsSource {\n list(): Skill[];\n readFile(name: string, relPath: string): SkillFile | null;\n readDir(\n name: string,\n relPath: string,\n ): { name: string; mimeType: string }[] | null;\n}\n\nexport function manifestSource(manifest: SkillsManifest): SkillsSource {\n const byName = new Map(manifest.map((s) => [s.name, s]));\n const fileKey = (relPath: string) => relPath.replace(/\\/+$/, \"\");\n\n return {\n list: () => manifest,\n readFile: (name, relPath) => byName.get(name)?.files[relPath] ?? null,\n readDir: (name, relPath) => {\n const skill = byName.get(name);\n if (!skill) {\n return null;\n }\n const prefix = relPath === \"\" ? \"\" : `${fileKey(relPath)}/`;\n const children = new Map<string, string>();\n let matched = relPath === \"\";\n for (const filePath of Object.keys(skill.files)) {\n if (!filePath.startsWith(prefix)) {\n continue;\n }\n matched = true;\n const rest = filePath.slice(prefix.length);\n const slash = rest.indexOf(\"/\");\n if (slash === -1) {\n children.set(rest, mimeTypeForFile(rest));\n } else {\n children.set(rest.slice(0, slash), \"inode/directory\");\n }\n }\n if (!matched) {\n return null;\n }\n return [...children].map(([name, mimeType]) => ({ name, mimeType }));\n },\n };\n}\n\nexport function diskSource(dir: string): SkillsSource {\n const rootReal = existsSync(dir) ? realpathSync(dir) : resolve(dir);\n\n // Confirms the real (symlink-followed) path stays within the skills root, else\n // null — closes the traversal hole where an in-tree symlink points outside it.\n const contain = (name: string, relPath: string): string | null => {\n const path = join(dir, name, relPath);\n if (!existsSync(path)) {\n return null;\n }\n const real = realpathSync(path);\n return real === rootReal || real.startsWith(`${rootReal}${sep}`)\n ? path\n : null;\n };\n\n return {\n list: () => discoverSkills(dir),\n readFile: (name, relPath) => {\n const path = contain(name, relPath || \"SKILL.md\");\n if (!path || !statSync(path).isFile()) {\n return null;\n }\n return {\n text: readFileSync(path, \"utf8\"),\n mimeType: mimeTypeForFile(path),\n };\n },\n readDir: (name, relPath) => {\n const path = contain(name, relPath);\n if (!path || !statSync(path).isDirectory()) {\n return null;\n }\n return readdirSync(path, { withFileTypes: true }).map((entry) => ({\n name: entry.name,\n mimeType: entry.isDirectory()\n ? \"inode/directory\"\n : mimeTypeForFile(entry.name),\n }));\n },\n };\n}\n\n/** The subset of `McpServer` that skill registration needs. */\ninterface SkillRegistrar {\n registerResource(\n name: string,\n uri: string,\n config: ResourceMetadata,\n readCallback: ReadResourceCallback,\n ): unknown;\n registerResource(\n name: string,\n template: ResourceTemplate,\n config: ResourceMetadata,\n readCallback: ReadResourceTemplateCallback,\n ): unknown;\n readonly server: SdkMcpServer[\"server\"];\n}\n\nconst DirectoryReadRequestSchema = z.object({\n method: z.literal(\"resources/directory/read\"),\n params: z.object({ uri: z.string(), cursor: z.string().optional() }),\n});\n\n// Registers per SEP-2640: one resource per `SKILL.md`, a template for supporting\n// files, the `skill://index.json` index, and optionally `resources/directory/read`.\nexport function registerSkills(\n server: SkillRegistrar,\n source: SkillsSource,\n { directoryRead }: { directoryRead: boolean },\n): void {\n for (const skill of source.list()) {\n const uri = `skill://${skill.name}/SKILL.md`;\n server.registerResource(\n skill.name,\n uri,\n {\n description:\n typeof skill.frontmatter.description === \"string\"\n ? skill.frontmatter.description\n : undefined,\n mimeType: \"text/markdown\",\n },\n (readUri) => {\n const file = source.readFile(skill.name, \"SKILL.md\");\n if (!file) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `Not found: ${readUri.href}`,\n );\n }\n return {\n contents: [\n { uri: readUri.href, text: file.text, mimeType: file.mimeType },\n ],\n };\n },\n );\n }\n\n server.registerResource(\n \"skill-files\",\n new ResourceTemplate(\"skill://{skillName}/{+filePath}\", {\n list: undefined,\n }),\n {},\n (readUri) => {\n const { name, relPath } = skillUriToRelPath(readUri.href);\n const file = source.readFile(name, relPath);\n if (!file) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `Not found: ${readUri.href}`,\n );\n }\n return {\n contents: [\n { uri: readUri.href, text: file.text, mimeType: file.mimeType },\n ],\n };\n },\n );\n\n server.registerResource(\n \"skill-index\",\n SKILL_INDEX_URI,\n { mimeType: \"application/json\" },\n () => ({\n contents: [\n {\n uri: SKILL_INDEX_URI,\n mimeType: \"application/json\",\n text: JSON.stringify({\n skills: source.list().map((skill) => ({\n url: `skill://${skill.name}/SKILL.md`,\n digest: skill.digest,\n frontmatter: skill.frontmatter,\n })),\n }),\n },\n ],\n }),\n );\n\n if (directoryRead) {\n server.server.setRequestHandler(\n DirectoryReadRequestSchema,\n ({ params }) => {\n const { name, relPath } = skillUriToRelPath(params.uri);\n const entries = source.readDir(name, relPath);\n if (!entries) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `Not a directory: ${params.uri}`,\n );\n }\n const base = params.uri.replace(/\\/+$/, \"\");\n return {\n resources: entries.map((entry) => ({\n uri: `${base}/${entry.name}`,\n name: entry.name,\n mimeType: entry.mimeType,\n })),\n };\n },\n );\n }\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
// @vitest-environment node
|
|
2
|
-
import { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
-
import { describe, expect, it, vi } from "vitest";
|
|
7
|
-
import { discoverSkills, diskSource, manifestSource, registerSkills, SKILL_INDEX_URI, skillUriToRelPath, } from "./skills.js";
|
|
8
|
-
function mkSkillDir(files) {
|
|
9
|
-
const root = mkdtempSync(join(tmpdir(), "skybridge-skills-"));
|
|
10
|
-
for (const [rel, content] of Object.entries(files)) {
|
|
11
|
-
const full = join(root, rel);
|
|
12
|
-
mkdirSync(join(full, ".."), { recursive: true });
|
|
13
|
-
writeFileSync(full, content);
|
|
14
|
-
}
|
|
15
|
-
return root;
|
|
16
|
-
}
|
|
17
|
-
const FM = (name, description) => `---\nname: ${name}\ndescription: ${description}\n---\n`;
|
|
18
|
-
describe("discoverSkills", () => {
|
|
19
|
-
it("discovers a skill with frontmatter, digest, and supporting files", () => {
|
|
20
|
-
const dir = mkSkillDir({
|
|
21
|
-
"git-workflow/SKILL.md": `${FM("git-workflow", "Team git conventions")}Body`,
|
|
22
|
-
"git-workflow/references/GUIDE.md": "# Guide",
|
|
23
|
-
});
|
|
24
|
-
const [skill, ...rest] = discoverSkills(dir);
|
|
25
|
-
expect(rest).toHaveLength(0);
|
|
26
|
-
expect(skill?.name).toBe("git-workflow");
|
|
27
|
-
expect(skill?.frontmatter).toMatchObject({
|
|
28
|
-
name: "git-workflow",
|
|
29
|
-
description: "Team git conventions",
|
|
30
|
-
});
|
|
31
|
-
expect(skill?.digest).toMatch(/^sha256:[a-f0-9]{64}$/);
|
|
32
|
-
expect(Object.keys(skill?.files ?? {}).sort()).toEqual([
|
|
33
|
-
"SKILL.md",
|
|
34
|
-
"references/GUIDE.md",
|
|
35
|
-
]);
|
|
36
|
-
});
|
|
37
|
-
it("returns [] for a missing directory and skips dirs without SKILL.md", () => {
|
|
38
|
-
expect(discoverSkills(join(tmpdir(), "does-not-exist-xyz"))).toEqual([]);
|
|
39
|
-
const dir = mkSkillDir({ "not-a-skill/README.md": "hi" });
|
|
40
|
-
expect(discoverSkills(dir)).toEqual([]);
|
|
41
|
-
});
|
|
42
|
-
it("parses a nested metadata map in frontmatter", () => {
|
|
43
|
-
const dir = mkSkillDir({
|
|
44
|
-
"s/SKILL.md": `---\nname: s\ndescription: d\nmetadata:\n version: "1.2"\n team: core\n---\n`,
|
|
45
|
-
});
|
|
46
|
-
expect(discoverSkills(dir)[0]?.frontmatter.metadata).toEqual({
|
|
47
|
-
version: "1.2",
|
|
48
|
-
team: "core",
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
it("throws on missing frontmatter", () => {
|
|
52
|
-
const dir = mkSkillDir({ "s/SKILL.md": "no frontmatter here" });
|
|
53
|
-
expect(() => discoverSkills(dir)).toThrow(/missing YAML frontmatter/);
|
|
54
|
-
});
|
|
55
|
-
it("throws when required fields are absent", () => {
|
|
56
|
-
const dir = mkSkillDir({ "s/SKILL.md": "---\nname: s\n---\n" });
|
|
57
|
-
expect(() => discoverSkills(dir)).toThrow(/Invalid skill frontmatter/);
|
|
58
|
-
});
|
|
59
|
-
it("throws when the frontmatter name does not match the directory", () => {
|
|
60
|
-
const dir = mkSkillDir({ "s/SKILL.md": FM("other", "d") });
|
|
61
|
-
expect(() => discoverSkills(dir)).toThrow(/must match its directory name/);
|
|
62
|
-
});
|
|
63
|
-
it("throws on unsupported (non-scalar) frontmatter values", () => {
|
|
64
|
-
const dir = mkSkillDir({
|
|
65
|
-
"s/SKILL.md": "---\nname: s\ndescription: d\ntags:\n - a\n - b\n---\n",
|
|
66
|
-
});
|
|
67
|
-
expect(() => discoverSkills(dir)).toThrow(/Cannot parse frontmatter/);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
describe("skillUriToRelPath", () => {
|
|
71
|
-
it("splits name and relative path", () => {
|
|
72
|
-
expect(skillUriToRelPath("skill://refunds/SKILL.md")).toEqual({
|
|
73
|
-
name: "refunds",
|
|
74
|
-
relPath: "SKILL.md",
|
|
75
|
-
});
|
|
76
|
-
expect(skillUriToRelPath("skill://refunds")).toEqual({
|
|
77
|
-
name: "refunds",
|
|
78
|
-
relPath: "",
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
it("rejects path traversal and malformed URIs", () => {
|
|
82
|
-
expect(() => skillUriToRelPath("skill://s/../../etc/passwd")).toThrow();
|
|
83
|
-
expect(() => skillUriToRelPath("skill://s/./x")).toThrow();
|
|
84
|
-
expect(() => skillUriToRelPath("https://example.com")).toThrow();
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
describe("registerSkills", () => {
|
|
88
|
-
const manifest = [
|
|
89
|
-
{
|
|
90
|
-
name: "refunds",
|
|
91
|
-
frontmatter: { name: "refunds", description: "Process refunds" },
|
|
92
|
-
digest: "sha256:abc",
|
|
93
|
-
files: {
|
|
94
|
-
"SKILL.md": { text: "# Refunds", mimeType: "text/markdown" },
|
|
95
|
-
"templates/email.md": { text: "Hi", mimeType: "text/markdown" },
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
];
|
|
99
|
-
function fakeRegistrar() {
|
|
100
|
-
const resources = new Map();
|
|
101
|
-
let dirHandler;
|
|
102
|
-
const server = {
|
|
103
|
-
registerResource: vi.fn((name, uri, _cfg, cb) => {
|
|
104
|
-
resources.set(name, {
|
|
105
|
-
uri,
|
|
106
|
-
cb: cb,
|
|
107
|
-
});
|
|
108
|
-
}),
|
|
109
|
-
server: {
|
|
110
|
-
setRequestHandler: vi.fn((_schema, handler) => {
|
|
111
|
-
dirHandler = handler;
|
|
112
|
-
}),
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
return { server, resources, getDirHandler: () => dirHandler };
|
|
116
|
-
}
|
|
117
|
-
it("builds an index.json with url, digest, and verbatim frontmatter", () => {
|
|
118
|
-
const { server, resources } = fakeRegistrar();
|
|
119
|
-
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
120
|
-
registerSkills(server, manifestSource(manifest), {
|
|
121
|
-
directoryRead: false,
|
|
122
|
-
});
|
|
123
|
-
const index = resources.get("skill-index");
|
|
124
|
-
const result = index?.cb(new URL(SKILL_INDEX_URI));
|
|
125
|
-
expect(JSON.parse(result.contents[0]?.text ?? "")).toEqual({
|
|
126
|
-
skills: [
|
|
127
|
-
{
|
|
128
|
-
url: "skill://refunds/SKILL.md",
|
|
129
|
-
digest: "sha256:abc",
|
|
130
|
-
frontmatter: { name: "refunds", description: "Process refunds" },
|
|
131
|
-
},
|
|
132
|
-
],
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
it("serves supporting files through the template resource", () => {
|
|
136
|
-
const { server, resources } = fakeRegistrar();
|
|
137
|
-
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
138
|
-
registerSkills(server, manifestSource(manifest), {
|
|
139
|
-
directoryRead: false,
|
|
140
|
-
});
|
|
141
|
-
const tpl = resources.get("skill-files");
|
|
142
|
-
expect(tpl?.uri).toBeInstanceOf(ResourceTemplate);
|
|
143
|
-
const url = new URL("skill://refunds/templates/email.md");
|
|
144
|
-
const result = tpl?.cb(url, {}, {});
|
|
145
|
-
expect(result.contents[0]?.text).toBe("Hi");
|
|
146
|
-
});
|
|
147
|
-
it("only registers the directory-read handler when enabled", () => {
|
|
148
|
-
const off = fakeRegistrar();
|
|
149
|
-
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
150
|
-
registerSkills(off.server, manifestSource(manifest), {
|
|
151
|
-
directoryRead: false,
|
|
152
|
-
});
|
|
153
|
-
expect(off.getDirHandler()).toBeUndefined();
|
|
154
|
-
const on = fakeRegistrar();
|
|
155
|
-
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
156
|
-
registerSkills(on.server, manifestSource(manifest), {
|
|
157
|
-
directoryRead: true,
|
|
158
|
-
});
|
|
159
|
-
const result = on.getDirHandler()?.({
|
|
160
|
-
params: { uri: "skill://refunds/templates" },
|
|
161
|
-
});
|
|
162
|
-
expect(result.resources).toEqual([
|
|
163
|
-
{
|
|
164
|
-
uri: "skill://refunds/templates/email.md",
|
|
165
|
-
name: "email.md",
|
|
166
|
-
mimeType: "text/markdown",
|
|
167
|
-
},
|
|
168
|
-
]);
|
|
169
|
-
});
|
|
170
|
-
it("lists a skill root and marks subdirectories as inode/directory", () => {
|
|
171
|
-
const { server, getDirHandler } = fakeRegistrar();
|
|
172
|
-
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
173
|
-
registerSkills(server, manifestSource(manifest), {
|
|
174
|
-
directoryRead: true,
|
|
175
|
-
});
|
|
176
|
-
const result = getDirHandler()?.({
|
|
177
|
-
params: { uri: "skill://refunds" },
|
|
178
|
-
});
|
|
179
|
-
expect(result.resources).toContainEqual({
|
|
180
|
-
uri: "skill://refunds/templates",
|
|
181
|
-
name: "templates",
|
|
182
|
-
mimeType: "inode/directory",
|
|
183
|
-
});
|
|
184
|
-
expect(result.resources).toContainEqual({
|
|
185
|
-
uri: "skill://refunds/SKILL.md",
|
|
186
|
-
name: "SKILL.md",
|
|
187
|
-
mimeType: "text/markdown",
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
describe("diskSource", () => {
|
|
192
|
-
it("reads files live and lists directories", () => {
|
|
193
|
-
const dir = mkSkillDir({
|
|
194
|
-
"refunds/SKILL.md": FM("refunds", "d"),
|
|
195
|
-
"refunds/templates/email.md": "Hi",
|
|
196
|
-
});
|
|
197
|
-
const source = diskSource(dir);
|
|
198
|
-
expect(source.list()).toHaveLength(1);
|
|
199
|
-
expect(source.readFile("refunds", "templates/email.md")?.text).toBe("Hi");
|
|
200
|
-
expect(source.readFile("refunds", "missing.md")).toBeNull();
|
|
201
|
-
expect(source.readDir("refunds", "nope")).toBeNull();
|
|
202
|
-
});
|
|
203
|
-
it("refuses to read or list through a symlink that escapes the skills root", () => {
|
|
204
|
-
const outside = mkdtempSync(join(tmpdir(), "skybridge-outside-"));
|
|
205
|
-
writeFileSync(join(outside, "secret.txt"), "TOP SECRET");
|
|
206
|
-
const dir = mkSkillDir({ "demo/SKILL.md": FM("demo", "d") });
|
|
207
|
-
symlinkSync(join(outside, "secret.txt"), join(dir, "demo", "leak.txt"));
|
|
208
|
-
symlinkSync(outside, join(dir, "demo", "escape"));
|
|
209
|
-
const source = diskSource(dir);
|
|
210
|
-
expect(source.readFile("demo", "leak.txt")).toBeNull();
|
|
211
|
-
expect(source.readDir("demo", "escape")).toBeNull();
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
//# sourceMappingURL=skills.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"skills.test.js","sourceRoot":"","sources":["../../src/server/skills.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { ResourceTemplate } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { describe, expect, it, vi } from \"vitest\";\nimport {\n discoverSkills,\n diskSource,\n manifestSource,\n registerSkills,\n SKILL_INDEX_URI,\n type Skill,\n skillUriToRelPath,\n} from \"./skills.js\";\n\nfunction mkSkillDir(files: Record<string, string>): string {\n const root = mkdtempSync(join(tmpdir(), \"skybridge-skills-\"));\n for (const [rel, content] of Object.entries(files)) {\n const full = join(root, rel);\n mkdirSync(join(full, \"..\"), { recursive: true });\n writeFileSync(full, content);\n }\n return root;\n}\n\nconst FM = (name: string, description: string) =>\n `---\\nname: ${name}\\ndescription: ${description}\\n---\\n`;\n\ndescribe(\"discoverSkills\", () => {\n it(\"discovers a skill with frontmatter, digest, and supporting files\", () => {\n const dir = mkSkillDir({\n \"git-workflow/SKILL.md\": `${FM(\"git-workflow\", \"Team git conventions\")}Body`,\n \"git-workflow/references/GUIDE.md\": \"# Guide\",\n });\n const [skill, ...rest] = discoverSkills(dir);\n expect(rest).toHaveLength(0);\n expect(skill?.name).toBe(\"git-workflow\");\n expect(skill?.frontmatter).toMatchObject({\n name: \"git-workflow\",\n description: \"Team git conventions\",\n });\n expect(skill?.digest).toMatch(/^sha256:[a-f0-9]{64}$/);\n expect(Object.keys(skill?.files ?? {}).sort()).toEqual([\n \"SKILL.md\",\n \"references/GUIDE.md\",\n ]);\n });\n\n it(\"returns [] for a missing directory and skips dirs without SKILL.md\", () => {\n expect(discoverSkills(join(tmpdir(), \"does-not-exist-xyz\"))).toEqual([]);\n const dir = mkSkillDir({ \"not-a-skill/README.md\": \"hi\" });\n expect(discoverSkills(dir)).toEqual([]);\n });\n\n it(\"parses a nested metadata map in frontmatter\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\": `---\\nname: s\\ndescription: d\\nmetadata:\\n version: \"1.2\"\\n team: core\\n---\\n`,\n });\n expect(discoverSkills(dir)[0]?.frontmatter.metadata).toEqual({\n version: \"1.2\",\n team: \"core\",\n });\n });\n\n it(\"throws on missing frontmatter\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": \"no frontmatter here\" });\n expect(() => discoverSkills(dir)).toThrow(/missing YAML frontmatter/);\n });\n\n it(\"throws when required fields are absent\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": \"---\\nname: s\\n---\\n\" });\n expect(() => discoverSkills(dir)).toThrow(/Invalid skill frontmatter/);\n });\n\n it(\"throws when the frontmatter name does not match the directory\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": FM(\"other\", \"d\") });\n expect(() => discoverSkills(dir)).toThrow(/must match its directory name/);\n });\n\n it(\"throws on unsupported (non-scalar) frontmatter values\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\": \"---\\nname: s\\ndescription: d\\ntags:\\n - a\\n - b\\n---\\n\",\n });\n expect(() => discoverSkills(dir)).toThrow(/Cannot parse frontmatter/);\n });\n});\n\ndescribe(\"skillUriToRelPath\", () => {\n it(\"splits name and relative path\", () => {\n expect(skillUriToRelPath(\"skill://refunds/SKILL.md\")).toEqual({\n name: \"refunds\",\n relPath: \"SKILL.md\",\n });\n expect(skillUriToRelPath(\"skill://refunds\")).toEqual({\n name: \"refunds\",\n relPath: \"\",\n });\n });\n\n it(\"rejects path traversal and malformed URIs\", () => {\n expect(() => skillUriToRelPath(\"skill://s/../../etc/passwd\")).toThrow();\n expect(() => skillUriToRelPath(\"skill://s/./x\")).toThrow();\n expect(() => skillUriToRelPath(\"https://example.com\")).toThrow();\n });\n});\n\ndescribe(\"registerSkills\", () => {\n const manifest: Skill[] = [\n {\n name: \"refunds\",\n frontmatter: { name: \"refunds\", description: \"Process refunds\" },\n digest: \"sha256:abc\",\n files: {\n \"SKILL.md\": { text: \"# Refunds\", mimeType: \"text/markdown\" },\n \"templates/email.md\": { text: \"Hi\", mimeType: \"text/markdown\" },\n },\n },\n ];\n\n function fakeRegistrar() {\n const resources = new Map<\n string,\n { uri: unknown; cb: (...args: unknown[]) => unknown }\n >();\n let dirHandler: ((req: unknown) => unknown) | undefined;\n const server = {\n registerResource: vi.fn(\n (name: string, uri: unknown, _cfg: unknown, cb: unknown) => {\n resources.set(name, {\n uri,\n cb: cb as (...args: unknown[]) => unknown,\n });\n },\n ),\n server: {\n setRequestHandler: vi.fn((_schema: unknown, handler: unknown) => {\n dirHandler = handler as (req: unknown) => unknown;\n }),\n },\n };\n return { server, resources, getDirHandler: () => dirHandler };\n }\n\n it(\"builds an index.json with url, digest, and verbatim frontmatter\", () => {\n const { server, resources } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifestSource(manifest), {\n directoryRead: false,\n });\n const index = resources.get(\"skill-index\");\n const result = index?.cb(new URL(SKILL_INDEX_URI)) as {\n contents: { text: string }[];\n };\n expect(JSON.parse(result.contents[0]?.text ?? \"\")).toEqual({\n skills: [\n {\n url: \"skill://refunds/SKILL.md\",\n digest: \"sha256:abc\",\n frontmatter: { name: \"refunds\", description: \"Process refunds\" },\n },\n ],\n });\n });\n\n it(\"serves supporting files through the template resource\", () => {\n const { server, resources } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifestSource(manifest), {\n directoryRead: false,\n });\n const tpl = resources.get(\"skill-files\");\n expect(tpl?.uri).toBeInstanceOf(ResourceTemplate);\n const url = new URL(\"skill://refunds/templates/email.md\");\n const result = tpl?.cb(url, {}, {}) as { contents: { text: string }[] };\n expect(result.contents[0]?.text).toBe(\"Hi\");\n });\n\n it(\"only registers the directory-read handler when enabled\", () => {\n const off = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(off.server as any, manifestSource(manifest), {\n directoryRead: false,\n });\n expect(off.getDirHandler()).toBeUndefined();\n\n const on = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(on.server as any, manifestSource(manifest), {\n directoryRead: true,\n });\n const result = on.getDirHandler()?.({\n params: { uri: \"skill://refunds/templates\" },\n }) as { resources: { uri: string; name: string; mimeType: string }[] };\n expect(result.resources).toEqual([\n {\n uri: \"skill://refunds/templates/email.md\",\n name: \"email.md\",\n mimeType: \"text/markdown\",\n },\n ]);\n });\n\n it(\"lists a skill root and marks subdirectories as inode/directory\", () => {\n const { server, getDirHandler } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifestSource(manifest), {\n directoryRead: true,\n });\n const result = getDirHandler()?.({\n params: { uri: \"skill://refunds\" },\n }) as { resources: { name: string; mimeType: string }[] };\n expect(result.resources).toContainEqual({\n uri: \"skill://refunds/templates\",\n name: \"templates\",\n mimeType: \"inode/directory\",\n });\n expect(result.resources).toContainEqual({\n uri: \"skill://refunds/SKILL.md\",\n name: \"SKILL.md\",\n mimeType: \"text/markdown\",\n });\n });\n});\n\ndescribe(\"diskSource\", () => {\n it(\"reads files live and lists directories\", () => {\n const dir = mkSkillDir({\n \"refunds/SKILL.md\": FM(\"refunds\", \"d\"),\n \"refunds/templates/email.md\": \"Hi\",\n });\n const source = diskSource(dir);\n expect(source.list()).toHaveLength(1);\n expect(source.readFile(\"refunds\", \"templates/email.md\")?.text).toBe(\"Hi\");\n expect(source.readFile(\"refunds\", \"missing.md\")).toBeNull();\n expect(source.readDir(\"refunds\", \"nope\")).toBeNull();\n });\n\n it(\"refuses to read or list through a symlink that escapes the skills root\", () => {\n const outside = mkdtempSync(join(tmpdir(), \"skybridge-outside-\"));\n writeFileSync(join(outside, \"secret.txt\"), \"TOP SECRET\");\n const dir = mkSkillDir({ \"demo/SKILL.md\": FM(\"demo\", \"d\") });\n symlinkSync(join(outside, \"secret.txt\"), join(dir, \"demo\", \"leak.txt\"));\n symlinkSync(outside, join(dir, \"demo\", \"escape\"));\n\n const source = diskSource(dir);\n expect(source.readFile(\"demo\", \"leak.txt\")).toBeNull();\n expect(source.readDir(\"demo\", \"escape\")).toBeNull();\n });\n});\n"]}
|