@tanstack/ai-skills 0.0.0 → 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/LICENSE +21 -0
- package/dist/esm/catalog.d.ts +5 -0
- package/dist/esm/catalog.js +18 -0
- package/dist/esm/catalog.js.map +1 -0
- package/dist/esm/combinators.d.ts +24 -0
- package/dist/esm/combinators.js +158 -0
- package/dist/esm/combinators.js.map +1 -0
- package/dist/esm/errors.d.ts +7 -0
- package/dist/esm/errors.js +2 -0
- package/dist/esm/index.d.ts +26 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/middleware.d.ts +44 -0
- package/dist/esm/middleware.js +133 -0
- package/dist/esm/middleware.js.map +1 -0
- package/dist/esm/node/index.d.ts +33 -0
- package/dist/esm/node/index.js +198 -0
- package/dist/esm/node/index.js.map +1 -0
- package/dist/esm/parse.d.ts +25 -0
- package/dist/esm/parse.js +155 -0
- package/dist/esm/parse.js.map +1 -0
- package/dist/esm/sources/inline.d.ts +9 -0
- package/dist/esm/sources/inline.js +48 -0
- package/dist/esm/sources/inline.js.map +1 -0
- package/dist/esm/static/index.d.ts +17 -0
- package/dist/esm/static/index.js +29 -0
- package/dist/esm/static/index.js.map +1 -0
- package/dist/esm/testing/index.d.ts +2 -0
- package/dist/esm/testing/index.js +78 -0
- package/dist/esm/testing/index.js.map +1 -0
- package/dist/esm/tools/load-skill.d.ts +11 -0
- package/dist/esm/tools/load-skill.js +67 -0
- package/dist/esm/tools/load-skill.js.map +1 -0
- package/dist/esm/tools/read-resource.d.ts +4 -0
- package/dist/esm/tools/read-resource.js +55 -0
- package/dist/esm/tools/read-resource.js.map +1 -0
- package/dist/esm/types.d.ts +70 -0
- package/dist/esm/types.js +13 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/util.d.ts +9 -0
- package/dist/esm/util.js +24 -0
- package/dist/esm/util.js.map +1 -0
- package/dist/esm/validate.d.ts +14 -0
- package/dist/esm/validate.js +33 -0
- package/dist/esm/validate.js.map +1 -0
- package/dist/esm/walk.d.ts +35 -0
- package/dist/esm/walk.js +49 -0
- package/dist/esm/walk.js.map +1 -0
- package/package.json +90 -1
- package/skills/ai-skills/SKILL.md +138 -0
- package/src/catalog.ts +44 -0
- package/src/combinators.ts +224 -0
- package/src/errors.ts +7 -0
- package/src/index.ts +49 -0
- package/src/middleware.ts +256 -0
- package/src/node/index.ts +281 -0
- package/src/parse.ts +247 -0
- package/src/sources/inline.ts +66 -0
- package/src/static/index.ts +64 -0
- package/src/testing/index.ts +92 -0
- package/src/tools/load-skill.ts +96 -0
- package/src/tools/read-resource.ts +64 -0
- package/src/types.ts +86 -0
- package/src/util.ts +28 -0
- package/src/validate.ts +63 -0
- package/src/walk.ts +84 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { parseSkill, stripFrontmatter } from "../parse.js";
|
|
2
|
+
import { walkSkillDirs } from "../walk.js";
|
|
3
|
+
import { assertSafeResourcePath, stableHash } from "../util.js";
|
|
4
|
+
import { readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
5
|
+
import { basename, join, relative, sep } from "node:path";
|
|
6
|
+
//#region src/node/index.ts
|
|
7
|
+
/**
|
|
8
|
+
* `skillDirectory` — a filesystem-backed {@link SkillSource}. Lives behind the
|
|
9
|
+
* `/node` subpath because it imports `node:fs`; the root export stays edge-safe
|
|
10
|
+
* (Workers, browsers), mirroring `@tanstack/ai-code-mode-snippets/storage`.
|
|
11
|
+
*/
|
|
12
|
+
var RESOURCE_DIRS = ["references", "assets"];
|
|
13
|
+
var SCRIPT_DIR = "scripts";
|
|
14
|
+
var nodeLister = async (dir) => {
|
|
15
|
+
return (await readdir(dir, { withFileTypes: true })).map((e) => ({
|
|
16
|
+
name: e.name,
|
|
17
|
+
path: join(dir, e.name),
|
|
18
|
+
type: e.isDirectory() ? "dir" : "file"
|
|
19
|
+
}));
|
|
20
|
+
};
|
|
21
|
+
/** Recursively collect file paths under `dir`, relative to `root`. */
|
|
22
|
+
async function collectFiles(dir, root) {
|
|
23
|
+
let ents;
|
|
24
|
+
try {
|
|
25
|
+
ents = await readdir(dir, { withFileTypes: true });
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const e of ents) {
|
|
31
|
+
const full = join(dir, e.name);
|
|
32
|
+
if (e.isDirectory()) out.push(...await collectFiles(full, root));
|
|
33
|
+
else out.push(relative(root, full).replace(/\\/g, "/"));
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function posixRel(path) {
|
|
38
|
+
return path.replace(/\\/g, "/");
|
|
39
|
+
}
|
|
40
|
+
function hasPrefix(rel, prefixes) {
|
|
41
|
+
const n = posixRel(rel);
|
|
42
|
+
return prefixes.some((p) => n === p || n.startsWith(`${p}/`));
|
|
43
|
+
}
|
|
44
|
+
async function resolveInside(dir, rel) {
|
|
45
|
+
const full = join(dir, rel);
|
|
46
|
+
const rootReal = await realpath(dir);
|
|
47
|
+
const fullReal = await realpath(full);
|
|
48
|
+
const prefix = rootReal.endsWith(sep) ? rootReal : rootReal + sep;
|
|
49
|
+
if (fullReal !== rootReal && !fullReal.startsWith(prefix)) throw new Error(`unsafe resource path: "${rel}"`);
|
|
50
|
+
return fullReal;
|
|
51
|
+
}
|
|
52
|
+
function skillDirectory(root, options = {}) {
|
|
53
|
+
const roots = Array.isArray(root) ? root : [root];
|
|
54
|
+
const { maxDepth, strict = true } = options;
|
|
55
|
+
/** Fresh scan of every root → parsed skill name → skill directory. First wins. */
|
|
56
|
+
const scan = async () => {
|
|
57
|
+
const map = /* @__PURE__ */ new Map();
|
|
58
|
+
for (const r of roots) {
|
|
59
|
+
const dirs = await walkSkillDirs(nodeLister, r, { maxDepth });
|
|
60
|
+
for (const d of dirs) {
|
|
61
|
+
const raw = await readFile(join(d.dir, "SKILL.md"), "utf8").catch(() => void 0);
|
|
62
|
+
if (raw === void 0) continue;
|
|
63
|
+
try {
|
|
64
|
+
const parsed = parseSkill(raw, {
|
|
65
|
+
dirName: basename(d.dir),
|
|
66
|
+
strict
|
|
67
|
+
});
|
|
68
|
+
if (!map.has(parsed.metadata.name)) map.set(parsed.metadata.name, d.dir);
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return map;
|
|
73
|
+
};
|
|
74
|
+
const dirOf = async (name) => {
|
|
75
|
+
const dir = (await scan()).get(name);
|
|
76
|
+
if (!dir) throw new Error(`no skill named "${name}" under ${roots.join(", ")}`);
|
|
77
|
+
return dir;
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
revision: async () => {
|
|
81
|
+
const map = await scan();
|
|
82
|
+
const parts = [];
|
|
83
|
+
for (const [name, dir] of [...map].sort()) {
|
|
84
|
+
const s = await stat(join(dir, "SKILL.md")).catch(() => void 0);
|
|
85
|
+
parts.push(`${name}:${s?.mtimeMs ?? 0}:${s?.size ?? 0}`);
|
|
86
|
+
}
|
|
87
|
+
return stableHash(parts.join("|"));
|
|
88
|
+
},
|
|
89
|
+
list: async () => {
|
|
90
|
+
const map = await scan();
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const [, dir] of map) {
|
|
93
|
+
const raw = await readFile(join(dir, "SKILL.md"), "utf8").catch(() => void 0);
|
|
94
|
+
if (raw === void 0) continue;
|
|
95
|
+
try {
|
|
96
|
+
out.push(parseSkill(raw, {
|
|
97
|
+
dirName: basename(dir),
|
|
98
|
+
strict
|
|
99
|
+
}).metadata);
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
},
|
|
104
|
+
load: async (name) => readFile(join(await dirOf(name), "SKILL.md"), "utf8"),
|
|
105
|
+
listResources: async (name) => {
|
|
106
|
+
const dir = await dirOf(name);
|
|
107
|
+
const files = [];
|
|
108
|
+
for (const sub of RESOURCE_DIRS) files.push(...await collectFiles(join(dir, sub), dir));
|
|
109
|
+
return files;
|
|
110
|
+
},
|
|
111
|
+
readResource: async (name, path) => {
|
|
112
|
+
assertSafeResourcePath(path);
|
|
113
|
+
if (!hasPrefix(path, RESOURCE_DIRS)) throw new Error(`resource path must be under references/ or assets/: "${path}"`);
|
|
114
|
+
const fullReal = await resolveInside(await dirOf(name), path);
|
|
115
|
+
if (hasPrefix(path, ["references"])) return readFile(fullReal, "utf8");
|
|
116
|
+
return readFile(fullReal);
|
|
117
|
+
},
|
|
118
|
+
listScripts: async (name) => {
|
|
119
|
+
const dir = await dirOf(name);
|
|
120
|
+
return (await collectFiles(join(dir, SCRIPT_DIR), dir)).map((p) => ({
|
|
121
|
+
path: p,
|
|
122
|
+
executable: false,
|
|
123
|
+
reason: "no-runtime"
|
|
124
|
+
}));
|
|
125
|
+
},
|
|
126
|
+
readScript: async (name, path) => {
|
|
127
|
+
assertSafeResourcePath(path);
|
|
128
|
+
if (!hasPrefix(path, [SCRIPT_DIR])) throw new Error(`script path must be under scripts/: "${path}"`);
|
|
129
|
+
const fullReal = await resolveInside(await dirOf(name), path);
|
|
130
|
+
const bytes = await readFile(fullReal);
|
|
131
|
+
return new Uint8Array(bytes);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Read a skill directory tree into a plain {@link GeneratedCatalog} — the shape
|
|
137
|
+
* `staticSkills` consumes. Used by the Vite plugin and directly available for
|
|
138
|
+
* custom build scripts.
|
|
139
|
+
*/
|
|
140
|
+
async function generateCatalog(root, options = {}) {
|
|
141
|
+
const roots = Array.isArray(root) ? root : [root];
|
|
142
|
+
const skills = [];
|
|
143
|
+
const seen = /* @__PURE__ */ new Set();
|
|
144
|
+
for (const r of roots) for (const { dir } of await walkSkillDirs(nodeLister, r, { maxDepth: options.maxDepth })) {
|
|
145
|
+
const raw = await readFile(join(dir, "SKILL.md"), "utf8").catch(() => void 0);
|
|
146
|
+
if (raw === void 0) continue;
|
|
147
|
+
let meta;
|
|
148
|
+
try {
|
|
149
|
+
meta = parseSkill(raw, {
|
|
150
|
+
dirName: basename(dir),
|
|
151
|
+
strict: options.strict ?? true
|
|
152
|
+
}).metadata;
|
|
153
|
+
} catch {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (seen.has(meta.name)) continue;
|
|
157
|
+
seen.add(meta.name);
|
|
158
|
+
const resources = {};
|
|
159
|
+
for (const sub of RESOURCE_DIRS) for (const rel of await collectFiles(join(dir, sub), dir)) resources[rel] = await readFile(join(dir, rel), "utf8").catch(() => "");
|
|
160
|
+
skills.push({
|
|
161
|
+
name: meta.name,
|
|
162
|
+
description: meta.description,
|
|
163
|
+
body: stripFrontmatter(raw),
|
|
164
|
+
...meta.compatibility && { compatibility: meta.compatibility },
|
|
165
|
+
...Object.keys(resources).length && { resources }
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
skills.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
169
|
+
return {
|
|
170
|
+
revision: stableHash(skills.map((s) => `${s.name}:${stableHash(s.body)}`).join("|")),
|
|
171
|
+
skills
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Vite plugin that globs `SKILL.md` under `dir` at build time and serves a
|
|
176
|
+
* virtual module (default id `virtual:tanstack-skills`) exporting the catalog
|
|
177
|
+
* `as const`. Consumers then wrap it with `staticSkills` for a literal-union of
|
|
178
|
+
* skill names. The catalog is embedded as JSON, so the bundle hash tracks it.
|
|
179
|
+
*/
|
|
180
|
+
function skillsCatalogPlugin(options = {}) {
|
|
181
|
+
const virtualId = options.virtualId ?? "virtual:tanstack-skills";
|
|
182
|
+
const resolved = `\0${virtualId}`;
|
|
183
|
+
const dir = options.dir ?? "skills";
|
|
184
|
+
return {
|
|
185
|
+
name: "tanstack-skills-catalog",
|
|
186
|
+
resolveId: (id) => id === virtualId ? resolved : void 0,
|
|
187
|
+
async load(id) {
|
|
188
|
+
if (id !== resolved) return void 0;
|
|
189
|
+
const catalog = await generateCatalog(dir, { maxDepth: options.maxDepth });
|
|
190
|
+
if (typeof this.addWatchFile === "function") for (const r of Array.isArray(dir) ? dir : [dir]) for (const d of await walkSkillDirs(nodeLister, r, { maxDepth: options.maxDepth })) this.addWatchFile(join(d.dir, "SKILL.md"));
|
|
191
|
+
return `export const catalog = ${JSON.stringify(catalog)} as const\n`;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
export { generateCatalog, skillDirectory, skillsCatalogPlugin };
|
|
197
|
+
|
|
198
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/node/index.ts"],"sourcesContent":["/**\n * `skillDirectory` — a filesystem-backed {@link SkillSource}. Lives behind the\n * `/node` subpath because it imports `node:fs`; the root export stays edge-safe\n * (Workers, browsers), mirroring `@tanstack/ai-code-mode-snippets/storage`.\n */\nimport { readFile, readdir, realpath, stat } from 'node:fs/promises'\nimport { basename, join, relative, sep } from 'node:path'\nimport { walkSkillDirs } from '../walk'\nimport { parseSkill, stripFrontmatter } from '../parse'\nimport { assertSafeResourcePath, stableHash } from '../util'\nimport type { Dirent } from 'node:fs'\nimport type { ListDir } from '../walk'\nimport type { GeneratedCatalog, GeneratedSkill } from '../static/index'\nimport type { SkillMetadata, SkillScriptRef, SkillSource } from '../types'\n\nconst RESOURCE_DIRS = ['references', 'assets']\nconst SCRIPT_DIR = 'scripts'\n\nexport interface SkillDirectoryOptions {\n maxDepth?: number\n /** default true — promote parse warnings to errors (see spec §7). */\n strict?: boolean\n}\n\nconst nodeLister: ListDir = async (dir) => {\n const ents = await readdir(dir, { withFileTypes: true })\n return ents.map((e) => ({\n name: e.name,\n path: join(dir, e.name),\n type: e.isDirectory() ? 'dir' : 'file',\n }))\n}\n\n/** Recursively collect file paths under `dir`, relative to `root`. */\nasync function collectFiles(dir: string, root: string): Promise<Array<string>> {\n let ents: Array<Dirent>\n try {\n ents = await readdir(dir, { withFileTypes: true })\n } catch {\n return []\n }\n const out: Array<string> = []\n for (const e of ents) {\n const full = join(dir, e.name)\n if (e.isDirectory()) out.push(...(await collectFiles(full, root)))\n else out.push(relative(root, full).replace(/\\\\/g, '/'))\n }\n return out\n}\n\nfunction posixRel(path: string): string {\n return path.replace(/\\\\/g, '/')\n}\n\nfunction hasPrefix(rel: string, prefixes: Array<string>): boolean {\n const n = posixRel(rel)\n return prefixes.some((p) => n === p || n.startsWith(`${p}/`))\n}\n\nasync function resolveInside(dir: string, rel: string): Promise<string> {\n const full = join(dir, rel)\n const rootReal = await realpath(dir)\n const fullReal = await realpath(full)\n const prefix = rootReal.endsWith(sep) ? rootReal : rootReal + sep\n if (fullReal !== rootReal && !fullReal.startsWith(prefix)) {\n throw new Error(`unsafe resource path: \"${rel}\"`)\n }\n return fullReal\n}\n\nexport function skillDirectory(\n root: string | Array<string>,\n options: SkillDirectoryOptions = {},\n): SkillSource {\n const roots = Array.isArray(root) ? root : [root]\n const { maxDepth, strict = true } = options\n\n /** Fresh scan of every root → parsed skill name → skill directory. First wins. */\n const scan = async (): Promise<Map<string, string>> => {\n const map = new Map<string, string>()\n for (const r of roots) {\n const dirs = await walkSkillDirs(nodeLister, r, { maxDepth })\n for (const d of dirs) {\n const raw = await readFile(join(d.dir, 'SKILL.md'), 'utf8').catch(\n () => undefined,\n )\n if (raw === undefined) continue\n try {\n const parsed = parseSkill(raw, {\n dirName: basename(d.dir),\n strict,\n })\n if (!map.has(parsed.metadata.name)) {\n map.set(parsed.metadata.name, d.dir)\n }\n } catch {\n // Skip unparseable skills (same policy as list()).\n }\n }\n }\n return map\n }\n\n const dirOf = async (name: string): Promise<string> => {\n const dir = (await scan()).get(name)\n if (!dir)\n throw new Error(`no skill named \"${name}\" under ${roots.join(', ')}`)\n return dir\n }\n\n return {\n revision: async () => {\n const map = await scan()\n const parts: Array<string> = []\n for (const [name, dir] of [...map].sort()) {\n const s = await stat(join(dir, 'SKILL.md')).catch(() => undefined)\n parts.push(`${name}:${s?.mtimeMs ?? 0}:${s?.size ?? 0}`)\n }\n return stableHash(parts.join('|'))\n },\n list: async () => {\n const map = await scan()\n const out: Array<SkillMetadata> = []\n for (const [, dir] of map) {\n const raw = await readFile(join(dir, 'SKILL.md'), 'utf8').catch(\n () => undefined,\n )\n if (raw === undefined) continue\n try {\n out.push(parseSkill(raw, { dirName: basename(dir), strict }).metadata)\n } catch {\n // Lenient: an unparseable skill is skipped, not fatal (spec §7).\n // strict mode still throws inside parseSkill for warnings, but a\n // genuinely broken file (no description) is always skipped.\n }\n }\n return out\n },\n load: async (name) => readFile(join(await dirOf(name), 'SKILL.md'), 'utf8'),\n listResources: async (name) => {\n const dir = await dirOf(name)\n const files: Array<string> = []\n for (const sub of RESOURCE_DIRS) {\n files.push(...(await collectFiles(join(dir, sub), dir)))\n }\n return files\n },\n readResource: async (name, path) => {\n assertSafeResourcePath(path)\n if (!hasPrefix(path, RESOURCE_DIRS)) {\n throw new Error(\n `resource path must be under references/ or assets/: \"${path}\"`,\n )\n }\n const dir = await dirOf(name)\n const fullReal = await resolveInside(dir, path)\n if (hasPrefix(path, ['references'])) {\n return readFile(fullReal, 'utf8')\n }\n return readFile(fullReal)\n },\n listScripts: async (name) => {\n const dir = await dirOf(name)\n const files = await collectFiles(join(dir, SCRIPT_DIR), dir)\n return files.map(\n (p): SkillScriptRef => ({\n path: p,\n executable: false,\n reason: 'no-runtime',\n }),\n )\n },\n readScript: async (name, path) => {\n assertSafeResourcePath(path)\n if (!hasPrefix(path, [SCRIPT_DIR])) {\n throw new Error(`script path must be under scripts/: \"${path}\"`)\n }\n const dir = await dirOf(name)\n const fullReal = await resolveInside(dir, path)\n const bytes = await readFile(fullReal)\n return new Uint8Array(bytes)\n },\n }\n}\n\n/**\n * Read a skill directory tree into a plain {@link GeneratedCatalog} — the shape\n * `staticSkills` consumes. Used by the Vite plugin and directly available for\n * custom build scripts.\n */\nexport async function generateCatalog(\n root: string | Array<string>,\n options: SkillDirectoryOptions = {},\n): Promise<GeneratedCatalog> {\n const roots = Array.isArray(root) ? root : [root]\n const skills: Array<GeneratedSkill> = []\n const seen = new Set<string>()\n for (const r of roots) {\n for (const { dir } of await walkSkillDirs(nodeLister, r, {\n maxDepth: options.maxDepth,\n })) {\n const raw = await readFile(join(dir, 'SKILL.md'), 'utf8').catch(\n () => undefined,\n )\n if (raw === undefined) continue\n let meta\n try {\n meta = parseSkill(raw, {\n dirName: basename(dir),\n strict: options.strict ?? true,\n }).metadata\n } catch {\n continue\n }\n if (seen.has(meta.name)) continue\n seen.add(meta.name)\n const resources: Record<string, string> = {}\n for (const sub of RESOURCE_DIRS) {\n for (const rel of await collectFiles(join(dir, sub), dir)) {\n resources[rel] = await readFile(join(dir, rel), 'utf8').catch(\n () => '',\n )\n }\n }\n skills.push({\n name: meta.name,\n description: meta.description,\n body: stripFrontmatter(raw),\n ...(meta.compatibility && { compatibility: meta.compatibility }),\n ...(Object.keys(resources).length && { resources }),\n })\n }\n }\n skills.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))\n const revision = stableHash(\n skills.map((s) => `${s.name}:${stableHash(s.body)}`).join('|'),\n )\n return { revision, skills }\n}\n\n/** Structural Vite plugin (no `vite` type dependency). */\nexport interface SkillsCatalogPlugin {\n name: string\n resolveId: (id: string) => string | undefined\n load: (\n this: { addWatchFile?: (id: string) => void },\n id: string,\n ) => Promise<string | undefined>\n}\n\n/**\n * Vite plugin that globs `SKILL.md` under `dir` at build time and serves a\n * virtual module (default id `virtual:tanstack-skills`) exporting the catalog\n * `as const`. Consumers then wrap it with `staticSkills` for a literal-union of\n * skill names. The catalog is embedded as JSON, so the bundle hash tracks it.\n */\nexport function skillsCatalogPlugin(\n options: { dir?: string; virtualId?: string; maxDepth?: number } = {},\n): SkillsCatalogPlugin {\n const virtualId = options.virtualId ?? 'virtual:tanstack-skills'\n const resolved = `\\0${virtualId}`\n const dir = options.dir ?? 'skills'\n return {\n name: 'tanstack-skills-catalog',\n resolveId: (id) => (id === virtualId ? resolved : undefined),\n async load(this: { addWatchFile?: (id: string) => void }, id: string) {\n if (id !== resolved) return undefined\n const catalog = await generateCatalog(dir, { maxDepth: options.maxDepth })\n if (typeof this.addWatchFile === 'function') {\n for (const r of Array.isArray(dir) ? dir : [dir]) {\n for (const d of await walkSkillDirs(nodeLister, r, {\n maxDepth: options.maxDepth,\n })) {\n this.addWatchFile(join(d.dir, 'SKILL.md'))\n }\n }\n }\n return `export const catalog = ${JSON.stringify(catalog)} as const\\n`\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SkillMetadata } from './types.js';
|
|
2
|
+
export interface ParseWarning {
|
|
3
|
+
code: 'name-dir-mismatch' | 'name-too-long' | 'name-invalid-chars';
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ParsedSkill {
|
|
7
|
+
metadata: SkillMetadata;
|
|
8
|
+
/** frontmatter stripped. */
|
|
9
|
+
body: string;
|
|
10
|
+
warnings: Array<ParseWarning>;
|
|
11
|
+
}
|
|
12
|
+
export declare class SkillParseError extends Error {
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parse a `SKILL.md`. Throws {@link SkillParseError} for cases the spec says to
|
|
17
|
+
* skip (no frontmatter, missing description). Non-fatal issues are returned as
|
|
18
|
+
* `warnings`; `strict` turns them into throws.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseSkill(raw: string, opts?: {
|
|
21
|
+
dirName?: string;
|
|
22
|
+
strict?: boolean;
|
|
23
|
+
}): ParsedSkill;
|
|
24
|
+
/** Strip the frontmatter block, returning just the body. */
|
|
25
|
+
export declare function stripFrontmatter(raw: string): string;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
//#region src/parse.ts
|
|
2
|
+
var SkillParseError = class extends Error {
|
|
3
|
+
name = "SkillParseError";
|
|
4
|
+
};
|
|
5
|
+
var NAME_RE = /^[a-z0-9-]+$/;
|
|
6
|
+
/** Split leading `---` frontmatter from the body. Returns null if absent. */
|
|
7
|
+
function splitFrontmatter(raw) {
|
|
8
|
+
const text = raw.replace(/^/, "");
|
|
9
|
+
const match = /^\s*---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n([\s\S]*))?$/.exec(text);
|
|
10
|
+
if (!match) return null;
|
|
11
|
+
return {
|
|
12
|
+
frontmatter: match[1] ?? "",
|
|
13
|
+
body: match[2] ?? ""
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Parse the flat frontmatter block into raw string/list/map values. */
|
|
17
|
+
function parseBlock(frontmatter) {
|
|
18
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
19
|
+
const out = {};
|
|
20
|
+
let i = 0;
|
|
21
|
+
const indentOf = (s) => s.length - s.trimStart().length;
|
|
22
|
+
while (i < lines.length) {
|
|
23
|
+
const line = lines[i];
|
|
24
|
+
if (line === void 0) break;
|
|
25
|
+
if (line.trim() === "" || line.trimStart().startsWith("#")) {
|
|
26
|
+
i++;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (indentOf(line) > 0) {
|
|
30
|
+
i++;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const colon = line.indexOf(":");
|
|
34
|
+
if (colon === -1) {
|
|
35
|
+
i++;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const key = line.slice(0, colon).trim();
|
|
39
|
+
const value = line.slice(colon + 1).trim();
|
|
40
|
+
if (value === ">" || value === "|" || /^[>|][+-]?\d*$/.test(value)) {
|
|
41
|
+
const folded = value.startsWith(">");
|
|
42
|
+
const collected = [];
|
|
43
|
+
i++;
|
|
44
|
+
while (i < lines.length) {
|
|
45
|
+
const next = lines[i];
|
|
46
|
+
if (next === void 0) break;
|
|
47
|
+
if (next.trim() !== "" && indentOf(next) === 0) break;
|
|
48
|
+
collected.push(next);
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
const nonEmpty = collected.filter((l) => l.trim() !== "");
|
|
52
|
+
const minIndent = nonEmpty.length ? Math.min(...nonEmpty.map(indentOf)) : 0;
|
|
53
|
+
const stripped = collected.map((l) => l.slice(minIndent));
|
|
54
|
+
out[key] = folded ? stripped.join(" ").replace(/\s+/g, " ").trim() : stripped.join("\n").trim();
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (value === "") {
|
|
58
|
+
const items = [];
|
|
59
|
+
const map = {};
|
|
60
|
+
let j = i + 1;
|
|
61
|
+
while (j < lines.length) {
|
|
62
|
+
const next = lines[j];
|
|
63
|
+
if (next === void 0) break;
|
|
64
|
+
if (next.trim() === "") {
|
|
65
|
+
j++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (indentOf(next) === 0) break;
|
|
69
|
+
const t = next.trim();
|
|
70
|
+
if (t.startsWith("- ")) items.push(unquote(t.slice(2).trim()));
|
|
71
|
+
else {
|
|
72
|
+
const c = t.indexOf(":");
|
|
73
|
+
if (c === -1) break;
|
|
74
|
+
map[t.slice(0, c).trim()] = unquote(t.slice(c + 1).trim());
|
|
75
|
+
}
|
|
76
|
+
j++;
|
|
77
|
+
}
|
|
78
|
+
if (items.length) out[key] = items;
|
|
79
|
+
else if (Object.keys(map).length) out[key] = map;
|
|
80
|
+
else out[key] = "";
|
|
81
|
+
i = Math.max(j, i + 1);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
85
|
+
out[key] = value.slice(1, -1).split(",").map((s) => unquote(s.trim())).filter((s) => s !== "");
|
|
86
|
+
i++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
out[key] = unquote(value);
|
|
90
|
+
i++;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
function unquote(s) {
|
|
95
|
+
if (s.startsWith("\"") && s.endsWith("\"") || s.startsWith("'") && s.endsWith("'")) return s.slice(1, -1);
|
|
96
|
+
return s;
|
|
97
|
+
}
|
|
98
|
+
function asStringMap(value) {
|
|
99
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
100
|
+
const out = {};
|
|
101
|
+
for (const [k, v] of Object.entries(value)) if (typeof v === "string") out[k] = v;
|
|
102
|
+
return Object.keys(out).length ? out : void 0;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Parse a `SKILL.md`. Throws {@link SkillParseError} for cases the spec says to
|
|
106
|
+
* skip (no frontmatter, missing description). Non-fatal issues are returned as
|
|
107
|
+
* `warnings`; `strict` turns them into throws.
|
|
108
|
+
*/
|
|
109
|
+
function parseSkill(raw, opts = {}) {
|
|
110
|
+
const split = splitFrontmatter(raw);
|
|
111
|
+
if (!split) throw new SkillParseError("SKILL.md has no frontmatter block");
|
|
112
|
+
const block = parseBlock(split.frontmatter);
|
|
113
|
+
const name = typeof block.name === "string" ? block.name : void 0;
|
|
114
|
+
const description = typeof block.description === "string" ? block.description : void 0;
|
|
115
|
+
if (!description) throw new SkillParseError("SKILL.md is missing a `description`");
|
|
116
|
+
const warnings = [];
|
|
117
|
+
const effectiveName = name ?? opts.dirName ?? "";
|
|
118
|
+
if (name && opts.dirName && name !== opts.dirName) warnings.push({
|
|
119
|
+
code: "name-dir-mismatch",
|
|
120
|
+
message: `skill name "${name}" does not match directory "${opts.dirName}"`
|
|
121
|
+
});
|
|
122
|
+
if (effectiveName.length > 64) warnings.push({
|
|
123
|
+
code: "name-too-long",
|
|
124
|
+
message: `skill name "${effectiveName}" exceeds 64 characters`
|
|
125
|
+
});
|
|
126
|
+
if (effectiveName && !NAME_RE.test(effectiveName)) warnings.push({
|
|
127
|
+
code: "name-invalid-chars",
|
|
128
|
+
message: `skill name "${effectiveName}" contains characters outside [a-z0-9-]`
|
|
129
|
+
});
|
|
130
|
+
if (opts.strict && warnings.length) throw new SkillParseError(warnings.map((w) => w.message).join("; "));
|
|
131
|
+
return {
|
|
132
|
+
metadata: {
|
|
133
|
+
name: effectiveName,
|
|
134
|
+
description,
|
|
135
|
+
...typeof block.license === "string" && { license: block.license },
|
|
136
|
+
...typeof block.compatibility === "string" && { compatibility: block.compatibility },
|
|
137
|
+
...Array.isArray(block.allowedTools) && { allowedTools: block.allowedTools.filter((t) => typeof t === "string") },
|
|
138
|
+
...(() => {
|
|
139
|
+
const m = asStringMap(block.metadata);
|
|
140
|
+
return m ? { metadata: m } : {};
|
|
141
|
+
})()
|
|
142
|
+
},
|
|
143
|
+
body: split.body.trim(),
|
|
144
|
+
warnings
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** Strip the frontmatter block, returning just the body. */
|
|
148
|
+
function stripFrontmatter(raw) {
|
|
149
|
+
const split = splitFrontmatter(raw);
|
|
150
|
+
return split ? split.body.trim() : raw.trim();
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
export { SkillParseError, parseSkill, stripFrontmatter };
|
|
154
|
+
|
|
155
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.js","names":[],"sources":["../../src/parse.ts"],"sourcesContent":["/**\n * Lenient `SKILL.md` frontmatter parsing (spec §7).\n *\n * Other clients emit malformed frontmatter — most commonly unquoted colons in\n * descriptions — so the default is lenient. We hand-roll a tiny parser rather\n * than pull in a YAML dependency: SKILL.md frontmatter is a flat key/value\n * block (plus block scalars for `description` and simple lists for\n * `allowedTools`), and taking `value = rest-of-line-after-first-colon` handles\n * the unquoted-colon case in a single pass — no quote-and-retry needed.\n *\n * | Condition | Behavior |\n * |----------------------|---------------------------------|\n * | name/dir mismatch | warn, load |\n * | name over 64 chars | warn, load |\n * | invalid name chars | warn, load |\n * | missing `description`| throw (caller skips) |\n * | no frontmatter | throw (caller skips) |\n *\n * `strict: true` promotes every warning to a thrown error.\n */\nimport type { SkillMetadata } from './types'\n\nexport interface ParseWarning {\n code: 'name-dir-mismatch' | 'name-too-long' | 'name-invalid-chars'\n message: string\n}\n\nexport interface ParsedSkill {\n metadata: SkillMetadata\n /** frontmatter stripped. */\n body: string\n warnings: Array<ParseWarning>\n}\n\nexport class SkillParseError extends Error {\n override name = 'SkillParseError'\n}\n\nconst NAME_RE = /^[a-z0-9-]+$/\n\n/** Split leading `---` frontmatter from the body. Returns null if absent. */\nfunction splitFrontmatter(\n raw: string,\n): { frontmatter: string; body: string } | null {\n // Tolerate a BOM and leading blank lines before the opening fence.\n const text = raw.replace(/^/, '')\n const match = /^\\s*---\\r?\\n([\\s\\S]*?)\\r?\\n---[ \\t]*(?:\\r?\\n([\\s\\S]*))?$/.exec(\n text,\n )\n if (!match) return null\n return { frontmatter: match[1] ?? '', body: match[2] ?? '' }\n}\n\n/** Parse the flat frontmatter block into raw string/list/map values. */\nfunction parseBlock(frontmatter: string): Record<string, unknown> {\n const lines = frontmatter.split(/\\r?\\n/)\n const out: Record<string, unknown> = {}\n let i = 0\n\n const indentOf = (s: string) => s.length - s.trimStart().length\n\n while (i < lines.length) {\n const line = lines[i]\n if (line === undefined) break\n if (line.trim() === '' || line.trimStart().startsWith('#')) {\n i++\n continue\n }\n // Top-level keys are unindented.\n if (indentOf(line) > 0) {\n i++\n continue\n }\n const colon = line.indexOf(':')\n if (colon === -1) {\n i++\n continue\n }\n const key = line.slice(0, colon).trim()\n const value = line.slice(colon + 1).trim()\n\n // Block scalar: `>` (folded) or `|` (literal), with optional chomp/indent.\n if (value === '>' || value === '|' || /^[>|][+-]?\\d*$/.test(value)) {\n const folded = value.startsWith('>')\n const collected: Array<string> = []\n i++\n while (i < lines.length) {\n const next = lines[i]\n if (next === undefined) break\n if (next.trim() !== '' && indentOf(next) === 0) break\n collected.push(next)\n i++\n }\n // Strip the common leading indentation of the block.\n const nonEmpty = collected.filter((l) => l.trim() !== '')\n const minIndent = nonEmpty.length\n ? Math.min(...nonEmpty.map(indentOf))\n : 0\n const stripped = collected.map((l) => l.slice(minIndent))\n out[key] = folded\n ? stripped.join(' ').replace(/\\s+/g, ' ').trim()\n : stripped.join('\\n').trim()\n continue\n }\n\n // Empty value → indented children: a block list (`- x`) or a nested map\n // (`k: v`), one level deep.\n if (value === '') {\n const items: Array<string> = []\n const map: Record<string, string> = {}\n let j = i + 1\n while (j < lines.length) {\n const next = lines[j]\n if (next === undefined) break\n if (next.trim() === '') {\n j++\n continue\n }\n if (indentOf(next) === 0) break\n const t = next.trim()\n if (t.startsWith('- ')) {\n items.push(unquote(t.slice(2).trim()))\n } else {\n const c = t.indexOf(':')\n if (c === -1) break\n map[t.slice(0, c).trim()] = unquote(t.slice(c + 1).trim())\n }\n j++\n }\n if (items.length) out[key] = items\n else if (Object.keys(map).length) out[key] = map\n else out[key] = ''\n i = Math.max(j, i + 1)\n continue\n }\n\n // Inline list `[a, b]`.\n if (value.startsWith('[') && value.endsWith(']')) {\n out[key] = value\n .slice(1, -1)\n .split(',')\n .map((s) => unquote(s.trim()))\n .filter((s) => s !== '')\n i++\n continue\n }\n\n out[key] = unquote(value)\n i++\n }\n return out\n}\n\nfunction unquote(s: string): string {\n if (\n (s.startsWith('\"') && s.endsWith('\"')) ||\n (s.startsWith(\"'\") && s.endsWith(\"'\"))\n ) {\n return s.slice(1, -1)\n }\n return s\n}\n\nfunction asStringMap(value: unknown): Record<string, string> | undefined {\n if (typeof value !== 'object' || value === null) return undefined\n const out: Record<string, string> = {}\n for (const [k, v] of Object.entries(value)) {\n if (typeof v === 'string') out[k] = v\n }\n return Object.keys(out).length ? out : undefined\n}\n\n/**\n * Parse a `SKILL.md`. Throws {@link SkillParseError} for cases the spec says to\n * skip (no frontmatter, missing description). Non-fatal issues are returned as\n * `warnings`; `strict` turns them into throws.\n */\nexport function parseSkill(\n raw: string,\n opts: { dirName?: string; strict?: boolean } = {},\n): ParsedSkill {\n const split = splitFrontmatter(raw)\n if (!split) {\n throw new SkillParseError('SKILL.md has no frontmatter block')\n }\n const block = parseBlock(split.frontmatter)\n\n const name = typeof block.name === 'string' ? block.name : undefined\n const description =\n typeof block.description === 'string' ? block.description : undefined\n\n if (!description) {\n throw new SkillParseError('SKILL.md is missing a `description`')\n }\n\n const warnings: Array<ParseWarning> = []\n const effectiveName = name ?? opts.dirName ?? ''\n\n if (name && opts.dirName && name !== opts.dirName) {\n warnings.push({\n code: 'name-dir-mismatch',\n message: `skill name \"${name}\" does not match directory \"${opts.dirName}\"`,\n })\n }\n if (effectiveName.length > 64) {\n warnings.push({\n code: 'name-too-long',\n message: `skill name \"${effectiveName}\" exceeds 64 characters`,\n })\n }\n if (effectiveName && !NAME_RE.test(effectiveName)) {\n warnings.push({\n code: 'name-invalid-chars',\n message: `skill name \"${effectiveName}\" contains characters outside [a-z0-9-]`,\n })\n }\n\n if (opts.strict && warnings.length) {\n throw new SkillParseError(warnings.map((w) => w.message).join('; '))\n }\n\n const metadata: SkillMetadata = {\n name: effectiveName,\n description,\n ...(typeof block.license === 'string' && { license: block.license }),\n ...(typeof block.compatibility === 'string' && {\n compatibility: block.compatibility,\n }),\n ...(Array.isArray(block.allowedTools) && {\n allowedTools: block.allowedTools.filter(\n (t): t is string => typeof t === 'string',\n ),\n }),\n ...((): { metadata?: Record<string, string> } => {\n const m = asStringMap(block.metadata)\n return m ? { metadata: m } : {}\n })(),\n }\n\n return { metadata, body: split.body.trim(), warnings }\n}\n\n/** Strip the frontmatter block, returning just the body. */\nexport function stripFrontmatter(raw: string): string {\n const split = splitFrontmatter(raw)\n return split ? split.body.trim() : raw.trim()\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SkillSource } from '../types.js';
|
|
2
|
+
export interface InlineSkillConfig {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
instructions: string;
|
|
6
|
+
resources?: Record<string, string | (() => string | Promise<string>)>;
|
|
7
|
+
compatibility?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function inlineSkill(config: InlineSkillConfig): SkillSource;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { stableHash } from "../util.js";
|
|
2
|
+
import { validateSkill } from "../validate.js";
|
|
3
|
+
//#region src/sources/inline.ts
|
|
4
|
+
/**
|
|
5
|
+
* `inlineSkill` — an edge-safe {@link SkillSource} for a single skill defined
|
|
6
|
+
* next to app code, in a DB row, or per-session/per-tenant. Resource values may
|
|
7
|
+
* be thunks, evaluated at read time.
|
|
8
|
+
*/
|
|
9
|
+
function inlineSkill(config) {
|
|
10
|
+
const metadata = {
|
|
11
|
+
name: config.name,
|
|
12
|
+
description: config.description,
|
|
13
|
+
...config.compatibility && { compatibility: config.compatibility }
|
|
14
|
+
};
|
|
15
|
+
const lint = validateSkill(metadata);
|
|
16
|
+
if (!lint.ok) throw new Error(`inlineSkill "${config.name}": ${lint.issues.map((i) => i.message).join("; ")}`);
|
|
17
|
+
const resourcePaths = Object.keys(config.resources ?? {});
|
|
18
|
+
const revision = stableHash(JSON.stringify({
|
|
19
|
+
metadata,
|
|
20
|
+
instructions: config.instructions,
|
|
21
|
+
resources: resourcePaths
|
|
22
|
+
}));
|
|
23
|
+
const assertName = (name) => {
|
|
24
|
+
if (name !== config.name) throw new Error(`inlineSkill has no skill named "${name}"`);
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
revision: () => Promise.resolve(revision),
|
|
28
|
+
list: () => Promise.resolve([metadata]),
|
|
29
|
+
load: async (name) => {
|
|
30
|
+
assertName(name);
|
|
31
|
+
return config.instructions;
|
|
32
|
+
},
|
|
33
|
+
listResources: async (name) => {
|
|
34
|
+
assertName(name);
|
|
35
|
+
return resourcePaths;
|
|
36
|
+
},
|
|
37
|
+
readResource: async (name, path) => {
|
|
38
|
+
assertName(name);
|
|
39
|
+
const value = config.resources?.[path];
|
|
40
|
+
if (value === void 0) throw new Error(`skill "${name}" has no resource "${path}"`);
|
|
41
|
+
return typeof value === "function" ? await value() : value;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { inlineSkill };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=inline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inline.js","names":[],"sources":["../../../src/sources/inline.ts"],"sourcesContent":["/**\n * `inlineSkill` — an edge-safe {@link SkillSource} for a single skill defined\n * next to app code, in a DB row, or per-session/per-tenant. Resource values may\n * be thunks, evaluated at read time.\n */\nimport { stableHash } from '../util'\nimport { validateSkill } from '../validate'\nimport type { SkillMetadata, SkillSource } from '../types'\n\nexport interface InlineSkillConfig {\n name: string\n description: string\n instructions: string\n resources?: Record<string, string | (() => string | Promise<string>)>\n compatibility?: string\n}\n\nexport function inlineSkill(config: InlineSkillConfig): SkillSource {\n const metadata: SkillMetadata = {\n name: config.name,\n description: config.description,\n ...(config.compatibility && { compatibility: config.compatibility }),\n }\n const lint = validateSkill(metadata)\n if (!lint.ok) {\n throw new Error(\n `inlineSkill \"${config.name}\": ${lint.issues.map((i) => i.message).join('; ')}`,\n )\n }\n const resourcePaths = Object.keys(config.resources ?? {})\n const revision = stableHash(\n JSON.stringify({\n metadata,\n instructions: config.instructions,\n resources: resourcePaths,\n }),\n )\n\n const assertName = (name: string) => {\n if (name !== config.name) {\n throw new Error(`inlineSkill has no skill named \"${name}\"`)\n }\n }\n\n return {\n revision: () => Promise.resolve(revision),\n list: () => Promise.resolve([metadata]),\n // Async so an unknown-name throw surfaces as a rejected promise.\n load: async (name) => {\n assertName(name)\n return config.instructions\n },\n listResources: async (name) => {\n assertName(name)\n return resourcePaths\n },\n readResource: async (name, path) => {\n assertName(name)\n const value = config.resources?.[path]\n if (value === undefined) {\n throw new Error(`skill \"${name}\" has no resource \"${path}\"`)\n }\n return typeof value === 'function' ? await value() : value\n },\n }\n}\n"],"mappings":";;;;;;;;AAiBA,SAAgB,YAAY,QAAwC;CAClE,MAAM,WAA0B;EAC9B,MAAM,OAAO;EACb,aAAa,OAAO;EACpB,GAAI,OAAO,iBAAiB,EAAE,eAAe,OAAO,cAAc;CACpE;CACA,MAAM,OAAO,cAAc,QAAQ;CACnC,IAAI,CAAC,KAAK,IACR,MAAM,IAAI,MACR,gBAAgB,OAAO,KAAK,KAAK,KAAK,OAAO,KAAK,MAAM,EAAE,OAAO,CAAC,CAAC,KAAK,IAAI,GAC9E;CAEF,MAAM,gBAAgB,OAAO,KAAK,OAAO,aAAa,CAAC,CAAC;CACxD,MAAM,WAAW,WACf,KAAK,UAAU;EACb;EACA,cAAc,OAAO;EACrB,WAAW;CACb,CAAC,CACH;CAEA,MAAM,cAAc,SAAiB;EACnC,IAAI,SAAS,OAAO,MAClB,MAAM,IAAI,MAAM,mCAAmC,KAAK,EAAE;CAE9D;CAEA,OAAO;EACL,gBAAgB,QAAQ,QAAQ,QAAQ;EACxC,YAAY,QAAQ,QAAQ,CAAC,QAAQ,CAAC;EAEtC,MAAM,OAAO,SAAS;GACpB,WAAW,IAAI;GACf,OAAO,OAAO;EAChB;EACA,eAAe,OAAO,SAAS;GAC7B,WAAW,IAAI;GACf,OAAO;EACT;EACA,cAAc,OAAO,MAAM,SAAS;GAClC,WAAW,IAAI;GACf,MAAM,QAAQ,OAAO,YAAY;GACjC,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,UAAU,KAAK,qBAAqB,KAAK,EAAE;GAE7D,OAAO,OAAO,UAAU,aAAa,MAAM,MAAM,IAAI;EACvD;CACF;AACF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SkillSource } from '../types.js';
|
|
2
|
+
export interface GeneratedSkill<T extends string = string> {
|
|
3
|
+
name: T;
|
|
4
|
+
description: string;
|
|
5
|
+
/** raw SKILL.md body, frontmatter stripped at generation time. */
|
|
6
|
+
body: string;
|
|
7
|
+
compatibility?: string;
|
|
8
|
+
/** embedded resource files, path → utf8 contents. */
|
|
9
|
+
resources?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export interface GeneratedCatalog<T extends string = string> {
|
|
12
|
+
revision: string;
|
|
13
|
+
skills: ReadonlyArray<GeneratedSkill<T>>;
|
|
14
|
+
}
|
|
15
|
+
export declare function staticSkills<T extends string>(catalog: GeneratedCatalog<T>): SkillSource & {
|
|
16
|
+
names: ReadonlyArray<T>;
|
|
17
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/static/index.ts
|
|
2
|
+
function staticSkills(catalog) {
|
|
3
|
+
const byName = new Map(catalog.skills.map((s) => [s.name, s]));
|
|
4
|
+
const get = (name) => {
|
|
5
|
+
const s = byName.get(name);
|
|
6
|
+
if (!s) throw new Error(`static catalog has no skill named "${name}"`);
|
|
7
|
+
return s;
|
|
8
|
+
};
|
|
9
|
+
return {
|
|
10
|
+
names: catalog.skills.map((s) => s.name),
|
|
11
|
+
revision: () => Promise.resolve(catalog.revision),
|
|
12
|
+
list: () => Promise.resolve(catalog.skills.map((s) => ({
|
|
13
|
+
name: s.name,
|
|
14
|
+
description: s.description,
|
|
15
|
+
...s.compatibility && { compatibility: s.compatibility }
|
|
16
|
+
}))),
|
|
17
|
+
load: async (name) => get(name).body,
|
|
18
|
+
listResources: async (name) => Object.keys(get(name).resources ?? {}),
|
|
19
|
+
readResource: async (name, path) => {
|
|
20
|
+
const value = get(name).resources?.[path];
|
|
21
|
+
if (value === void 0) throw new Error(`skill "${name}" has no resource "${path}"`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { staticSkills };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/static/index.ts"],"sourcesContent":["/**\n * `staticSkills` — wrap a build-time-generated catalog as a {@link SkillSource}.\n *\n * Edge-safe: this file imports no `node:*` modules. The Vite plugin that GLOBS\n * `skills/**/SKILL.md` and emits the catalog lives in `@tanstack/ai-skills/node`\n * (it needs `node:fs`); the emitted catalog is plain data consumed here.\n *\n * The generated catalog is `as const`, so `T` is the literal union of skill\n * names — which flows into `load_skill`'s enum, the constraint the spec\n * recommends, obtained at build time rather than runtime.\n */\nimport type { SkillMetadata, SkillSource } from '../types'\n\nexport interface GeneratedSkill<T extends string = string> {\n name: T\n description: string\n /** raw SKILL.md body, frontmatter stripped at generation time. */\n body: string\n compatibility?: string\n /** embedded resource files, path → utf8 contents. */\n resources?: Record<string, string>\n}\n\nexport interface GeneratedCatalog<T extends string = string> {\n revision: string\n skills: ReadonlyArray<GeneratedSkill<T>>\n}\n\nexport function staticSkills<T extends string>(\n catalog: GeneratedCatalog<T>,\n): SkillSource & { names: ReadonlyArray<T> } {\n const byName = new Map(catalog.skills.map((s) => [s.name, s]))\n const get = (name: string): GeneratedSkill<T> => {\n const s = byName.get(name as T)\n if (!s) throw new Error(`static catalog has no skill named \"${name}\"`)\n return s\n }\n\n return {\n names: catalog.skills.map((s) => s.name),\n revision: () => Promise.resolve(catalog.revision),\n list: () =>\n Promise.resolve(\n catalog.skills.map(\n (s): SkillMetadata => ({\n name: s.name,\n description: s.description,\n ...(s.compatibility && { compatibility: s.compatibility }),\n }),\n ),\n ),\n // Methods are async so a missing-skill throw surfaces as a rejected\n // promise (what callers `await`), not a synchronous throw.\n load: async (name) => get(name).body,\n listResources: async (name) => Object.keys(get(name).resources ?? {}),\n readResource: async (name, path) => {\n const value = get(name).resources?.[path]\n if (value === undefined) {\n throw new Error(`skill \"${name}\" has no resource \"${path}\"`)\n }\n return value\n },\n }\n}\n"],"mappings":";AA4BA,SAAgB,aACd,SAC2C;CAC3C,MAAM,SAAS,IAAI,IAAI,QAAQ,OAAO,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;CAC7D,MAAM,OAAO,SAAoC;EAC/C,MAAM,IAAI,OAAO,IAAI,IAAS;EAC9B,IAAI,CAAC,GAAG,MAAM,IAAI,MAAM,sCAAsC,KAAK,EAAE;EACrE,OAAO;CACT;CAEA,OAAO;EACL,OAAO,QAAQ,OAAO,KAAK,MAAM,EAAE,IAAI;EACvC,gBAAgB,QAAQ,QAAQ,QAAQ,QAAQ;EAChD,YACE,QAAQ,QACN,QAAQ,OAAO,KACZ,OAAsB;GACrB,MAAM,EAAE;GACR,aAAa,EAAE;GACf,GAAI,EAAE,iBAAiB,EAAE,eAAe,EAAE,cAAc;EAC1D,EACF,CACF;EAGF,MAAM,OAAO,SAAS,IAAI,IAAI,CAAC,CAAC;EAChC,eAAe,OAAO,SAAS,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,aAAa,CAAC,CAAC;EACpE,cAAc,OAAO,MAAM,SAAS;GAClC,MAAM,QAAQ,IAAI,IAAI,CAAC,CAAC,YAAY;GACpC,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,UAAU,KAAK,qBAAqB,KAAK,EAAE;GAE7D,OAAO;EACT;CACF;AACF"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
//#region src/testing/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* `runSkillSourceConformance` — the real deliverable for third-party adapters.
|
|
5
|
+
* Since adapter code is frequently LLM-generated, this suite (not prose) is what
|
|
6
|
+
* makes a new `SkillSource` safe to ship.
|
|
7
|
+
*
|
|
8
|
+
* The factory must return a source seeded with this fixed fixture contract:
|
|
9
|
+
*
|
|
10
|
+
* - skill `alpha`: description non-empty; resource `references/note.md` whose
|
|
11
|
+
* contents are exactly `hello`; script `scripts/run.py` whose bytes decode
|
|
12
|
+
* to `print(1)` (only if the source supports scripts).
|
|
13
|
+
* - skill `beta`: description non-empty; no resources required.
|
|
14
|
+
*
|
|
15
|
+
* Sources that cannot represent a tier (resources/scripts) simply omit the
|
|
16
|
+
* corresponding methods — those cases are skipped, not failed.
|
|
17
|
+
*/
|
|
18
|
+
var dec = (v) => typeof v === "string" ? v : new TextDecoder().decode(v);
|
|
19
|
+
function runSkillSourceConformance(factory, label = "SkillSource") {
|
|
20
|
+
describe(`conformance: ${label}`, () => {
|
|
21
|
+
it("lists skills with a name and description", async () => {
|
|
22
|
+
const skills = await (await factory()).list();
|
|
23
|
+
const names = skills.map((s) => s.name);
|
|
24
|
+
expect(names).toContain("alpha");
|
|
25
|
+
expect(names).toContain("beta");
|
|
26
|
+
for (const s of skills) {
|
|
27
|
+
expect(s.name).toBeTruthy();
|
|
28
|
+
expect(s.description).toBeTruthy();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
it("loads a known skill body", async () => {
|
|
32
|
+
const body = await (await factory()).load("alpha");
|
|
33
|
+
expect(typeof body).toBe("string");
|
|
34
|
+
expect(body.length).toBeGreaterThan(0);
|
|
35
|
+
});
|
|
36
|
+
it("throws (not returns empty) for a missing skill name", async () => {
|
|
37
|
+
const source = await factory();
|
|
38
|
+
await expect(source.load("does-not-exist")).rejects.toThrow();
|
|
39
|
+
});
|
|
40
|
+
it("has a stable revision across identical content", async () => {
|
|
41
|
+
const rev = (await factory()).revision;
|
|
42
|
+
if (!rev) return;
|
|
43
|
+
const a = await rev();
|
|
44
|
+
const b = await rev();
|
|
45
|
+
expect(a).toBe(b);
|
|
46
|
+
const other = await factory();
|
|
47
|
+
if (other.revision) expect(await other.revision()).toBe(a);
|
|
48
|
+
});
|
|
49
|
+
it("serves concurrent list() consistently", async () => {
|
|
50
|
+
const source = await factory();
|
|
51
|
+
const [a, b] = await Promise.all([source.list(), source.list()]);
|
|
52
|
+
expect(a.map((s) => s.name).sort()).toEqual(b.map((s) => s.name).sort());
|
|
53
|
+
});
|
|
54
|
+
it("reads a bundled resource and rejects path traversal", async () => {
|
|
55
|
+
const { listResources, readResource } = await factory();
|
|
56
|
+
if (!listResources || !readResource) return;
|
|
57
|
+
const resources = await listResources("alpha");
|
|
58
|
+
expect(resources).toContain("references/note.md");
|
|
59
|
+
const value = await readResource("alpha", "references/note.md");
|
|
60
|
+
expect(dec(value).trimEnd()).toBe("hello");
|
|
61
|
+
await expect(readResource("alpha", "../../etc/passwd")).rejects.toThrow();
|
|
62
|
+
});
|
|
63
|
+
it("returns script bytes correctly", async () => {
|
|
64
|
+
const source = await factory();
|
|
65
|
+
if (!source.listScripts || !source.readScript) return;
|
|
66
|
+
const ref = (await source.listScripts("alpha")).find((s) => s.path === "scripts/run.py");
|
|
67
|
+
if (!ref) return;
|
|
68
|
+
expect(ref.executable).toBe(false);
|
|
69
|
+
const bytes = await source.readScript("alpha", "scripts/run.py");
|
|
70
|
+
expect(bytes).toBeInstanceOf(Uint8Array);
|
|
71
|
+
expect(dec(bytes)).toContain("print(1)");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { runSkillSourceConformance };
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=index.js.map
|