@webskill/sdk 0.0.6 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -46
- package/dist/browser.d.ts +5 -1
- package/dist/browser.js +9 -2
- package/dist/{dist-33_zuOCm.js → dist-Cc3Mqi3_.js} +5 -1
- package/dist/{dist-nXiR40hi.js → dist-D5fsiETF.js} +36 -752
- package/dist/{dist-BQruQ9Hv.js → dist-Dv4a1Jkm.js} +15 -3
- package/dist/governance.d.ts +3 -2
- package/dist/governance.js +2 -2
- package/dist/{index-vi7GPemR.d.ts → index-Dc4X2N54.d.ts} +3 -1
- package/dist/{index-DCifjtJx.d.ts → index-oG5Nzgb9.d.ts} +19 -595
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/mcp.d.ts +3 -1
- package/dist/mcp.js +3 -1
- package/dist/memoryArtifactStore-C9lFVqPF-U8nXvqL9.js +637 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +3 -2
- package/dist/testing-pn3NhXSV.js +117 -0
- package/dist/testing.d.ts +73 -0
- package/dist/testing.js +4 -0
- package/dist/types-CgNC-oQu-Dq_A1yA-.d.ts +539 -0
- package/dist/ui-react.d.ts +1 -1
- package/dist/ui-react.js +1 -1
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +1 -1
- package/dist/ui.d.ts +17 -4
- package/dist/ui.js +2 -2
- package/package.json +5 -1
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import { parse } from "yaml";
|
|
2
|
+
import { zipSync } from "fflate";
|
|
3
|
+
|
|
4
|
+
//#region ../core/dist/index.js
|
|
5
|
+
/**
|
|
6
|
+
* 所有公开 API 抛出的结构化错误,code 供上层可编程处理
|
|
7
|
+
* @stable
|
|
8
|
+
*/
|
|
9
|
+
var WebSkillError = class extends Error {
|
|
10
|
+
code;
|
|
11
|
+
details;
|
|
12
|
+
constructor(code, message, details) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "WebSkillError";
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.details = details;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/** 技能目录内的安装清单文件名(不参与 files 列表与 digest) */
|
|
20
|
+
const SKILL_MANIFEST_FILE = "webskill.skill-manifest.json";
|
|
21
|
+
/** managed root 下的锁定文件名(不参与 files 列表与 digest) */
|
|
22
|
+
const SKILLS_LOCKFILE = "skills.lock.json";
|
|
23
|
+
/** 整体 digest:逐文件 "<path>:<sha256>\n" 拼接串(文件按 path 排序)的 sha256(hash 由调用方提供,可为 WebCrypto 异步实现) */
|
|
24
|
+
async function computeDigest(files, sha256) {
|
|
25
|
+
return sha256([...files].sort((a, b) => a.path.localeCompare(b.path)).map((f) => `${f.path}:${f.sha256}\n`).join(""));
|
|
26
|
+
}
|
|
27
|
+
/** 由逐文件清单构建 manifest(digest 由 computeDigest 计算) */
|
|
28
|
+
async function buildManifest(input) {
|
|
29
|
+
return {
|
|
30
|
+
schemaVersion: 1,
|
|
31
|
+
name: input.name,
|
|
32
|
+
...input.version !== void 0 ? { version: input.version } : {},
|
|
33
|
+
source: input.source,
|
|
34
|
+
installedAt: input.installedAt,
|
|
35
|
+
integrity: {
|
|
36
|
+
algorithm: "sha256",
|
|
37
|
+
digest: await computeDigest(input.files, input.sha256)
|
|
38
|
+
},
|
|
39
|
+
files: input.files
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** 按 manifest.files 比对实际 hash(调用方负责计算 actualHashes) */
|
|
43
|
+
function verifyManifest(manifest, actualHashes) {
|
|
44
|
+
const mismatches = [];
|
|
45
|
+
for (const file of manifest.files) {
|
|
46
|
+
const actual = actualHashes.get(file.path);
|
|
47
|
+
if (actual === void 0 || actual !== file.sha256) mismatches.push(file.path);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
ok: mismatches.length === 0,
|
|
51
|
+
mismatches
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const ROOT = "/";
|
|
55
|
+
/** 内存文件系统:路径一律规范化为 `/` 分隔,写入自动创建父目录 */
|
|
56
|
+
var MemoryFS = class {
|
|
57
|
+
kind = "memory";
|
|
58
|
+
#entries = /* @__PURE__ */ new Map();
|
|
59
|
+
constructor() {
|
|
60
|
+
this.#entries.set(ROOT, {
|
|
61
|
+
type: "directory",
|
|
62
|
+
content: /* @__PURE__ */ new Uint8Array(0),
|
|
63
|
+
mtimeMs: Date.now()
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
#normalize(path) {
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const seg of path.replace(/\\/g, "/").split("/")) {
|
|
69
|
+
if (seg === "" || seg === ".") continue;
|
|
70
|
+
if (seg === "..") out.pop();
|
|
71
|
+
else out.push(seg);
|
|
72
|
+
}
|
|
73
|
+
return ROOT + out.join("/");
|
|
74
|
+
}
|
|
75
|
+
#parentOf(key) {
|
|
76
|
+
const i = key.lastIndexOf("/");
|
|
77
|
+
return i <= 0 ? ROOT : key.slice(0, i);
|
|
78
|
+
}
|
|
79
|
+
#ensureParents(key) {
|
|
80
|
+
const chain = [];
|
|
81
|
+
let dir = this.#parentOf(key);
|
|
82
|
+
while (!this.#entries.has(dir)) {
|
|
83
|
+
chain.push(dir);
|
|
84
|
+
dir = this.#parentOf(dir);
|
|
85
|
+
}
|
|
86
|
+
for (const d of chain.reverse()) this.#entries.set(d, {
|
|
87
|
+
type: "directory",
|
|
88
|
+
content: /* @__PURE__ */ new Uint8Array(0),
|
|
89
|
+
mtimeMs: Date.now()
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
#getFile(key) {
|
|
93
|
+
const entry = this.#entries.get(key);
|
|
94
|
+
if (!entry || entry.type !== "file") throw new WebSkillError("FS_NOT_FOUND", `File not found: ${key}`);
|
|
95
|
+
return entry;
|
|
96
|
+
}
|
|
97
|
+
#getDirectory(key) {
|
|
98
|
+
const entry = this.#entries.get(key);
|
|
99
|
+
if (!entry || entry.type !== "directory") throw new WebSkillError("FS_NOT_FOUND", `Directory not found: ${key}`);
|
|
100
|
+
return entry;
|
|
101
|
+
}
|
|
102
|
+
async readText(path) {
|
|
103
|
+
return new TextDecoder().decode(this.#getFile(this.#normalize(path)).content);
|
|
104
|
+
}
|
|
105
|
+
async writeText(path, content) {
|
|
106
|
+
await this.writeBinary(path, new TextEncoder().encode(content));
|
|
107
|
+
}
|
|
108
|
+
async readBinary(path) {
|
|
109
|
+
return this.#getFile(this.#normalize(path)).content;
|
|
110
|
+
}
|
|
111
|
+
async writeBinary(path, content) {
|
|
112
|
+
const key = this.#normalize(path);
|
|
113
|
+
this.#ensureParents(key);
|
|
114
|
+
this.#entries.set(key, {
|
|
115
|
+
type: "file",
|
|
116
|
+
content,
|
|
117
|
+
mtimeMs: Date.now()
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async exists(path) {
|
|
121
|
+
return this.#entries.has(this.#normalize(path));
|
|
122
|
+
}
|
|
123
|
+
async stat(path) {
|
|
124
|
+
const key = this.#normalize(path);
|
|
125
|
+
const entry = this.#entries.get(key);
|
|
126
|
+
if (!entry) throw new WebSkillError("FS_NOT_FOUND", `Path not found: ${key}`);
|
|
127
|
+
return {
|
|
128
|
+
path: key,
|
|
129
|
+
type: entry.type,
|
|
130
|
+
size: entry.type === "file" ? entry.content.length : void 0,
|
|
131
|
+
mtimeMs: entry.mtimeMs
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async list(path) {
|
|
135
|
+
const key = this.#normalize(path);
|
|
136
|
+
this.#getDirectory(key);
|
|
137
|
+
const prefix = key === ROOT ? ROOT : key + "/";
|
|
138
|
+
const out = [];
|
|
139
|
+
for (const [k, entry] of this.#entries) {
|
|
140
|
+
if (k === key || !k.startsWith(prefix)) continue;
|
|
141
|
+
if (k.slice(prefix.length).includes("/")) continue;
|
|
142
|
+
out.push({
|
|
143
|
+
path: k,
|
|
144
|
+
type: entry.type,
|
|
145
|
+
size: entry.type === "file" ? entry.content.length : void 0,
|
|
146
|
+
mtimeMs: entry.mtimeMs
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
async mkdir(path) {
|
|
152
|
+
const key = this.#normalize(path);
|
|
153
|
+
if (this.#entries.has(key)) return;
|
|
154
|
+
this.#ensureParents(key);
|
|
155
|
+
this.#entries.set(key, {
|
|
156
|
+
type: "directory",
|
|
157
|
+
content: /* @__PURE__ */ new Uint8Array(0),
|
|
158
|
+
mtimeMs: Date.now()
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async remove(path, options) {
|
|
162
|
+
const key = this.#normalize(path);
|
|
163
|
+
if (!this.#entries.get(key)) throw new WebSkillError("FS_NOT_FOUND", `Path not found: ${key}`);
|
|
164
|
+
if (key === ROOT) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", "Cannot delete root directory");
|
|
165
|
+
const prefix = key + "/";
|
|
166
|
+
const descendants = [...this.#entries.keys()].filter((k) => k.startsWith(prefix));
|
|
167
|
+
if (descendants.length > 0 && !options?.recursive) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Directory not empty, use recursive: ${key}`);
|
|
168
|
+
for (const k of descendants) this.#entries.delete(k);
|
|
169
|
+
this.#entries.delete(key);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
/** 统一分隔符为 `/`,去除 `.` 段与重复分隔符(不解析 `..`) */
|
|
173
|
+
function normalizePath(path) {
|
|
174
|
+
return path.replace(/\\/g, "/").split("/").filter((s) => s !== "" && s !== ".").join("/");
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* 将相对路径安全地解析到 root 之内。
|
|
178
|
+
* 拒绝空路径、绝对路径(`/`、`\\`、`C:\` 形式)与任何 `..` 段。
|
|
179
|
+
*/
|
|
180
|
+
function resolveInsideRoot(root, relativePath) {
|
|
181
|
+
const fail = (reason) => {
|
|
182
|
+
throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Path outside root: ${reason} (path: ${JSON.stringify(relativePath)})`);
|
|
183
|
+
};
|
|
184
|
+
if (relativePath.trim() === "") fail("empty path");
|
|
185
|
+
if (relativePath.startsWith("/") || relativePath.startsWith("\\") || /^[a-zA-Z]:[\\/]/.test(relativePath)) fail("absolute paths not allowed");
|
|
186
|
+
const segments = relativePath.replace(/\\/g, "/").split("/");
|
|
187
|
+
if (segments.some((s) => s === "..")) fail("`..` segments not allowed");
|
|
188
|
+
const clean = segments.filter((s) => s !== "" && s !== ".");
|
|
189
|
+
if (clean.length === 0) fail("path resolves to empty");
|
|
190
|
+
const normalizedRoot = normalizePath(root);
|
|
191
|
+
const joined = normalizedRoot === "" ? clean.join("/") : `${normalizedRoot}/${clean.join("/")}`;
|
|
192
|
+
return root.startsWith("/") || root.startsWith("\\") ? `/${joined}` : joined;
|
|
193
|
+
}
|
|
194
|
+
const FRONTMATTER_RE = /^---[^\S\r\n]*\r?\n([\s\S]*?)\r?\n---[^\S\r\n]*(?:\r?\n|$)([\s\S]*)$/;
|
|
195
|
+
const invalid = (message, details) => {
|
|
196
|
+
throw new WebSkillError("SKILL_INVALID_METADATA", message, details);
|
|
197
|
+
};
|
|
198
|
+
function parseSkillMarkdown(raw) {
|
|
199
|
+
const match = FRONTMATTER_RE.exec(raw);
|
|
200
|
+
if (!match) invalid("SKILL.md missing YAML frontmatter (must start with a `---` line)");
|
|
201
|
+
const [, yamlText, bodyRaw] = match;
|
|
202
|
+
let data;
|
|
203
|
+
try {
|
|
204
|
+
data = parse(yamlText);
|
|
205
|
+
} catch (cause) {
|
|
206
|
+
invalid("frontmatter YAML parse failed", cause);
|
|
207
|
+
}
|
|
208
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) invalid("frontmatter must be a YAML object");
|
|
209
|
+
const metadata = data;
|
|
210
|
+
if (typeof metadata["name"] !== "string" || metadata["name"].trim() === "") invalid("frontmatter missing required `name` field");
|
|
211
|
+
if (typeof metadata["description"] !== "string" || metadata["description"].trim() === "") invalid("frontmatter missing required `description` field");
|
|
212
|
+
return {
|
|
213
|
+
metadata,
|
|
214
|
+
body: bodyRaw.trim()
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const SKILL_NAME_MAX_LENGTH = 64;
|
|
218
|
+
/** Agent Skills 官方命名规范:小写字母/数字/连字符,不允许首尾或连续连字符 */
|
|
219
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
220
|
+
function isValidSkillName(name) {
|
|
221
|
+
return name.length <= 64 && SKILL_NAME_PATTERN.test(name);
|
|
222
|
+
}
|
|
223
|
+
const SCRIPT_EXTENSION_RE = /\.(ts|js)$/;
|
|
224
|
+
/** D2 schema sidecar(scripts/<name>.schema.json)属于协议内文件,不算非法脚本 */
|
|
225
|
+
const SCHEMA_SIDECAR_RE = /\.schema\.json$/;
|
|
226
|
+
/**
|
|
227
|
+
* 技能合规规则的唯一实现。SkillDiscovery 与 validateSkills 都必须调用本函数,
|
|
228
|
+
* 禁止各自重复实现规则(避免两套规则漂移)。
|
|
229
|
+
*/
|
|
230
|
+
function checkSkillRules(input) {
|
|
231
|
+
const { dirName } = input;
|
|
232
|
+
if (!input.hasSkillMd) return [{
|
|
233
|
+
code: "SKILL_NOT_FOUND",
|
|
234
|
+
severity: "warning",
|
|
235
|
+
message: `Directory ${dirName} missing SKILL.md, not treated as a skill, skipped`
|
|
236
|
+
}];
|
|
237
|
+
if (input.parseError) return [{
|
|
238
|
+
code: input.parseError.code,
|
|
239
|
+
severity: "error",
|
|
240
|
+
message: `SKILL.md parse failed for directory ${dirName}: ${input.parseError.message}`
|
|
241
|
+
}];
|
|
242
|
+
const issues = [];
|
|
243
|
+
const metadata = input.metadata;
|
|
244
|
+
if (metadata) {
|
|
245
|
+
if (!isValidSkillName(metadata.name)) issues.push({
|
|
246
|
+
code: "SKILL_INVALID_NAME",
|
|
247
|
+
severity: "error",
|
|
248
|
+
message: `Skill name ${JSON.stringify(metadata.name)} does not conform to naming convention (lowercase letters/digits/hyphens, ≤64 chars)`
|
|
249
|
+
});
|
|
250
|
+
if (metadata.name !== dirName) issues.push({
|
|
251
|
+
code: "SKILL_INVALID_NAME",
|
|
252
|
+
severity: "error",
|
|
253
|
+
message: `Skill name ${JSON.stringify(metadata.name)} does not match directory name ${JSON.stringify(dirName)}`
|
|
254
|
+
});
|
|
255
|
+
if (input.existingNames?.has(metadata.name)) issues.push({
|
|
256
|
+
code: "SKILL_DUPLICATE_NAME",
|
|
257
|
+
severity: "error",
|
|
258
|
+
message: `Duplicate skill name ${JSON.stringify(metadata.name)}`
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
for (const fileName of input.scriptFileNames ?? []) if (!SCRIPT_EXTENSION_RE.test(fileName) && !SCHEMA_SIDECAR_RE.test(fileName)) issues.push({
|
|
262
|
+
code: "SKILL_UNSUPPORTED_SCRIPT",
|
|
263
|
+
severity: "error",
|
|
264
|
+
message: `Unsupported script file in scripts/ of directory ${dirName}: ${fileName} (only .ts/.js and .schema.json sidecars allowed)`
|
|
265
|
+
});
|
|
266
|
+
const allowedTools = metadata?.["allowed-tools"];
|
|
267
|
+
if (Array.isArray(allowedTools)) {
|
|
268
|
+
const scriptNames = new Set((input.scriptFileNames ?? []).filter((f) => SCRIPT_EXTENSION_RE.test(f)).map((f) => f.replace(SCRIPT_EXTENSION_RE, "")));
|
|
269
|
+
for (const entry of allowedTools) if (typeof entry === "string" && !scriptNames.has(entry)) issues.push({
|
|
270
|
+
code: "SKILL_UNKNOWN_ALLOWED_TOOL",
|
|
271
|
+
severity: "warning",
|
|
272
|
+
message: `allowed-tools entry ${JSON.stringify(entry)} of directory ${dirName} does not match any script in scripts/`
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (metadata && input.knownSkillNames) {
|
|
276
|
+
const dependencies = metadata["dependencies"];
|
|
277
|
+
if (Array.isArray(dependencies)) {
|
|
278
|
+
for (const dep of dependencies) if (typeof dep === "string" && !input.knownSkillNames.has(dep)) issues.push({
|
|
279
|
+
code: "SKILL_UNKNOWN_DEPENDENCY",
|
|
280
|
+
severity: "error",
|
|
281
|
+
message: `Skill ${JSON.stringify(metadata.name)} depends on unknown skill ${JSON.stringify(dep)}`
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return issues;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* 循环依赖检测(全量邻接表,discovery 两趟扫描第二趟调用)。
|
|
289
|
+
* 每个环只报一次(error 级,阻断进 Catalog);自引用视为长度 1 的环。
|
|
290
|
+
*/
|
|
291
|
+
function checkDependencyCycles(adjacency) {
|
|
292
|
+
const issues = [];
|
|
293
|
+
const involved = /* @__PURE__ */ new Set();
|
|
294
|
+
const reported = /* @__PURE__ */ new Set();
|
|
295
|
+
const state = /* @__PURE__ */ new Map();
|
|
296
|
+
const stack = [];
|
|
297
|
+
const visit = (node) => {
|
|
298
|
+
if (state.get(node) === "done") return;
|
|
299
|
+
if (state.get(node) === "visiting") {
|
|
300
|
+
const cycle = [...stack.slice(stack.indexOf(node)), node];
|
|
301
|
+
const key = cycle.slice(0, -1).sort().join("");
|
|
302
|
+
if (!reported.has(key)) {
|
|
303
|
+
reported.add(key);
|
|
304
|
+
for (const name of cycle.slice(0, -1)) involved.add(name);
|
|
305
|
+
issues.push({
|
|
306
|
+
code: "SKILL_CIRCULAR_DEPENDENCY",
|
|
307
|
+
severity: "error",
|
|
308
|
+
message: `Circular skill dependency detected: ${cycle.join(" -> ")}`
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
state.set(node, "visiting");
|
|
314
|
+
stack.push(node);
|
|
315
|
+
for (const dep of adjacency.get(node) ?? []) visit(dep);
|
|
316
|
+
stack.pop();
|
|
317
|
+
state.set(node, "done");
|
|
318
|
+
};
|
|
319
|
+
for (const node of adjacency.keys()) visit(node);
|
|
320
|
+
return {
|
|
321
|
+
issues,
|
|
322
|
+
involved
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* 技能包集(skill pack)格式与打包逻辑(node/browser 共用单一来源):
|
|
327
|
+
* webskill.skill-pack.json { schemaVersion: 1, skills: [{ name, digest }] }
|
|
328
|
+
* <skill-a>/SKILL.md … webskill.skill-manifest.json
|
|
329
|
+
* <skill-b>/SKILL.md …
|
|
330
|
+
* fflate 环境无关;文件读取经 FileSystemProvider 抽象。
|
|
331
|
+
*/
|
|
332
|
+
/** 包级清单文件名(位于 zip 根,不参与各技能 digest) */
|
|
333
|
+
const SKILL_PACK_FILE = "webskill.skill-pack.json";
|
|
334
|
+
const messageOf = (e) => e instanceof Error ? e.message : String(e);
|
|
335
|
+
const baseName$1 = (p) => p.split("/").pop() ?? p;
|
|
336
|
+
async function listFilesRecursive(fs, root, prefix = "") {
|
|
337
|
+
const out = [];
|
|
338
|
+
for (const entry of await fs.list(root)) {
|
|
339
|
+
const rel = prefix === "" ? baseName$1(entry.path) : `${prefix}/${baseName$1(entry.path)}`;
|
|
340
|
+
if (entry.type === "directory") out.push(...await listFilesRecursive(fs, entry.path, rel));
|
|
341
|
+
else out.push(rel);
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* 多技能打包为单一 zip(包级清单 + 各技能目录含自身 manifest)。
|
|
347
|
+
* manifestBuilder 负责确保技能目录内 manifest 文件存在并返回之(digest 入包级清单)。
|
|
348
|
+
* @stable
|
|
349
|
+
*/
|
|
350
|
+
async function exportSkills(fs, input) {
|
|
351
|
+
if (input.roots.length === 0) throw new WebSkillError("EXPORT_FAILED", "exportSkills requires at least one skill root");
|
|
352
|
+
const files = {};
|
|
353
|
+
const skills = [];
|
|
354
|
+
try {
|
|
355
|
+
for (const root of input.roots) {
|
|
356
|
+
const base = root.replace(/\/+$/, "");
|
|
357
|
+
const manifest = await input.manifestBuilder(base);
|
|
358
|
+
skills.push({
|
|
359
|
+
name: manifest.name,
|
|
360
|
+
digest: manifest.integrity.digest
|
|
361
|
+
});
|
|
362
|
+
for (const rel of await listFilesRecursive(fs, base)) files[`${manifest.name}/${rel}`] = await fs.readBinary(`${base}/${rel}`);
|
|
363
|
+
}
|
|
364
|
+
} catch (e) {
|
|
365
|
+
if (e instanceof WebSkillError) throw e;
|
|
366
|
+
throw new WebSkillError("EXPORT_FAILED", `Failed to export skill pack: ${messageOf(e)}`, e);
|
|
367
|
+
}
|
|
368
|
+
const pack = {
|
|
369
|
+
schemaVersion: 1,
|
|
370
|
+
skills
|
|
371
|
+
};
|
|
372
|
+
files[SKILL_PACK_FILE] = new TextEncoder().encode(JSON.stringify(pack, null, 2));
|
|
373
|
+
return zipSync(files, { level: 6 });
|
|
374
|
+
}
|
|
375
|
+
/** 包级清单解析 + 形状校验;损坏 → INSTALL_FAILED(安装侧保证零残留) */
|
|
376
|
+
function parseSkillPackManifest(text) {
|
|
377
|
+
let raw;
|
|
378
|
+
try {
|
|
379
|
+
raw = JSON.parse(text);
|
|
380
|
+
} catch (e) {
|
|
381
|
+
throw new WebSkillError("INSTALL_FAILED", `Skill pack manifest is not valid JSON: ${messageOf(e)}`, e);
|
|
382
|
+
}
|
|
383
|
+
const invalid = (detail) => {
|
|
384
|
+
throw new WebSkillError("INSTALL_FAILED", `Skill pack manifest is corrupted: ${detail}`);
|
|
385
|
+
};
|
|
386
|
+
if (typeof raw !== "object" || raw === null) invalid("not an object");
|
|
387
|
+
const record = raw;
|
|
388
|
+
if (record["schemaVersion"] !== 1) invalid("schemaVersion must be 1");
|
|
389
|
+
if (!Array.isArray(record["skills"]) || record["skills"].length === 0) invalid("\"skills\" must be a non-empty array");
|
|
390
|
+
for (const entry of record["skills"]) {
|
|
391
|
+
if (typeof entry !== "object" || entry === null) invalid("skill entry is not an object");
|
|
392
|
+
const { name, digest } = entry;
|
|
393
|
+
if (typeof name !== "string" || name === "") invalid("skill entry missing a valid \"name\"");
|
|
394
|
+
if (typeof digest !== "string" || digest === "") invalid(`skill "${name}" missing a valid "digest"`);
|
|
395
|
+
}
|
|
396
|
+
return raw;
|
|
397
|
+
}
|
|
398
|
+
/** 由条目列表构建 Catalog,保证按名称排序 */
|
|
399
|
+
function buildCatalog(entries) {
|
|
400
|
+
return { entries: [...entries].sort((a, b) => a.name.localeCompare(b.name)) };
|
|
401
|
+
}
|
|
402
|
+
/** 按技能名读取技能根目录内的文件,全部读取强制经过路径安全检查 */
|
|
403
|
+
var SkillReader = class {
|
|
404
|
+
#fs;
|
|
405
|
+
#index;
|
|
406
|
+
constructor(fs, index) {
|
|
407
|
+
this.#fs = fs;
|
|
408
|
+
this.#index = index;
|
|
409
|
+
}
|
|
410
|
+
#rootFor(skillName) {
|
|
411
|
+
const root = this.#index.get(skillName);
|
|
412
|
+
if (!root) throw new WebSkillError("SKILL_NOT_FOUND", `Unknown skill: ${skillName}`);
|
|
413
|
+
return root;
|
|
414
|
+
}
|
|
415
|
+
async readSkillMarkdown(skillName) {
|
|
416
|
+
return this.readSkillFile(skillName, "SKILL.md");
|
|
417
|
+
}
|
|
418
|
+
async readSkillFile(skillName, relativePath) {
|
|
419
|
+
return this.#fs.readText(resolveInsideRoot(this.#rootFor(skillName), relativePath));
|
|
420
|
+
}
|
|
421
|
+
async readSkillBinary(skillName, relativePath) {
|
|
422
|
+
return this.#fs.readBinary(resolveInsideRoot(this.#rootFor(skillName), relativePath));
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const baseName = (path) => path.split("/").pop() ?? path;
|
|
426
|
+
var SkillDiscovery = class {
|
|
427
|
+
#fs;
|
|
428
|
+
#roots;
|
|
429
|
+
/** name → skillRoot,与 SkillReader 共享同一 Map 引用 */
|
|
430
|
+
#index = /* @__PURE__ */ new Map();
|
|
431
|
+
#reader;
|
|
432
|
+
constructor(fs, roots) {
|
|
433
|
+
this.#fs = fs;
|
|
434
|
+
this.#roots = roots;
|
|
435
|
+
this.#reader = new SkillReader(fs, this.#index);
|
|
436
|
+
}
|
|
437
|
+
async discover() {
|
|
438
|
+
this.#index.clear();
|
|
439
|
+
const entries = [];
|
|
440
|
+
const issues = [];
|
|
441
|
+
const candidates = [];
|
|
442
|
+
const knownSkillNames = /* @__PURE__ */ new Set();
|
|
443
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
444
|
+
for (const root of this.#roots) {
|
|
445
|
+
if (!await this.#fs.exists(root)) {
|
|
446
|
+
issues.push({
|
|
447
|
+
code: "FS_NOT_FOUND",
|
|
448
|
+
severity: "warning",
|
|
449
|
+
message: `Skill root directory not found, skipped: ${root}`,
|
|
450
|
+
path: root
|
|
451
|
+
});
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
for (const child of await this.#fs.list(root)) {
|
|
455
|
+
if (child.type !== "directory") continue;
|
|
456
|
+
const dirName = baseName(child.path);
|
|
457
|
+
const skillRoot = `${root}/${dirName}`;
|
|
458
|
+
const skillMdPath = `${skillRoot}/SKILL.md`;
|
|
459
|
+
const hasSkillMd = await this.#fs.exists(skillMdPath);
|
|
460
|
+
let metadata;
|
|
461
|
+
let parseError;
|
|
462
|
+
if (hasSkillMd) try {
|
|
463
|
+
metadata = parseSkillMarkdown(await this.#fs.readText(skillMdPath)).metadata;
|
|
464
|
+
} catch (e) {
|
|
465
|
+
parseError = e instanceof WebSkillError ? e : new WebSkillError("SKILL_INVALID_METADATA", String(e));
|
|
466
|
+
}
|
|
467
|
+
candidates.push({
|
|
468
|
+
dirName,
|
|
469
|
+
skillRoot,
|
|
470
|
+
hasSkillMd,
|
|
471
|
+
metadata,
|
|
472
|
+
parseError,
|
|
473
|
+
scriptFileNames: await this.#listScriptFileNames(skillRoot)
|
|
474
|
+
});
|
|
475
|
+
if (metadata) {
|
|
476
|
+
knownSkillNames.add(metadata.name);
|
|
477
|
+
const dependencies = metadata["dependencies"];
|
|
478
|
+
adjacency.set(metadata.name, Array.isArray(dependencies) ? dependencies.filter((d) => typeof d === "string") : []);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const cycles = checkDependencyCycles(adjacency);
|
|
483
|
+
const claimedNames = /* @__PURE__ */ new Set();
|
|
484
|
+
for (const candidate of candidates) {
|
|
485
|
+
const { metadata } = candidate;
|
|
486
|
+
const candidateIssues = checkSkillRules({
|
|
487
|
+
dirName: candidate.dirName,
|
|
488
|
+
hasSkillMd: candidate.hasSkillMd,
|
|
489
|
+
metadata,
|
|
490
|
+
parseError: candidate.parseError,
|
|
491
|
+
scriptFileNames: candidate.scriptFileNames,
|
|
492
|
+
existingNames: claimedNames,
|
|
493
|
+
knownSkillNames
|
|
494
|
+
});
|
|
495
|
+
for (const issue of candidateIssues) issue.path ??= candidate.skillRoot;
|
|
496
|
+
issues.push(...candidateIssues);
|
|
497
|
+
if (metadata) claimedNames.add(metadata.name);
|
|
498
|
+
const excludedByCycle = metadata !== void 0 && cycles.involved.has(metadata.name);
|
|
499
|
+
if (candidateIssues.some((i) => i.severity === "error") || !metadata || excludedByCycle) continue;
|
|
500
|
+
this.#index.set(metadata.name, candidate.skillRoot);
|
|
501
|
+
entries.push({
|
|
502
|
+
name: metadata.name,
|
|
503
|
+
description: metadata.description,
|
|
504
|
+
version: typeof metadata["version"] === "string" ? metadata["version"] : void 0,
|
|
505
|
+
license: typeof metadata["license"] === "string" ? metadata["license"] : void 0,
|
|
506
|
+
root: candidate.skillRoot,
|
|
507
|
+
source: "local",
|
|
508
|
+
hasScripts: candidate.scriptFileNames.length > 0,
|
|
509
|
+
hasReferences: await this.#fs.exists(`${candidate.skillRoot}/references`),
|
|
510
|
+
hasAssets: await this.#fs.exists(`${candidate.skillRoot}/assets`)
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
issues.push(...cycles.issues);
|
|
514
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
515
|
+
return {
|
|
516
|
+
entries,
|
|
517
|
+
issues
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
async catalog() {
|
|
521
|
+
const { entries, issues } = await this.discover();
|
|
522
|
+
return {
|
|
523
|
+
catalog: buildCatalog(entries),
|
|
524
|
+
issues
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/** 按需加载技能完整文档;技能不存在时抛 SKILL_NOT_FOUND */
|
|
528
|
+
async loadDocument(skillName) {
|
|
529
|
+
if (this.#index.size === 0) await this.discover();
|
|
530
|
+
const { metadata, body } = parseSkillMarkdown(await this.#reader.readSkillMarkdown(skillName));
|
|
531
|
+
const skillRoot = this.#index.get(skillName);
|
|
532
|
+
return {
|
|
533
|
+
metadata,
|
|
534
|
+
body,
|
|
535
|
+
location: {
|
|
536
|
+
skillRoot,
|
|
537
|
+
skillMdPath: `${skillRoot}/SKILL.md`,
|
|
538
|
+
source: "local"
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
async #listScriptFileNames(skillRoot) {
|
|
543
|
+
const scriptsDir = `${skillRoot}/scripts`;
|
|
544
|
+
if (!await this.#fs.exists(scriptsDir)) return [];
|
|
545
|
+
return (await this.#fs.list(scriptsDir)).filter((s) => s.type === "file").map((s) => baseName(s.path));
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
/**
|
|
549
|
+
* 合规校验。内部直接复用 SkillDiscovery 的扫描逻辑,
|
|
550
|
+
* 规则判定同样落在 checkSkillRules 单一来源上。
|
|
551
|
+
*/
|
|
552
|
+
async function validateSkills(fs, roots) {
|
|
553
|
+
const { issues } = await new SkillDiscovery(fs, roots).discover();
|
|
554
|
+
return {
|
|
555
|
+
ok: !issues.some((i) => i.severity === "error"),
|
|
556
|
+
issues
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function renderCatalogJson(catalog) {
|
|
560
|
+
return JSON.stringify(catalog, null, 2);
|
|
561
|
+
}
|
|
562
|
+
const jsonRenderer = {
|
|
563
|
+
format: "json",
|
|
564
|
+
render: renderCatalogJson
|
|
565
|
+
};
|
|
566
|
+
/** 转义 XML 五个特殊字符:& < > " ' */
|
|
567
|
+
function escapeXml(text) {
|
|
568
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
569
|
+
}
|
|
570
|
+
function renderAvailableSkillsXml(catalog) {
|
|
571
|
+
const lines = ["<available_skills>"];
|
|
572
|
+
for (const skill of catalog.entries) {
|
|
573
|
+
lines.push(" <skill>");
|
|
574
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
575
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
576
|
+
if (skill.version !== void 0) lines.push(` <version>${escapeXml(skill.version)}</version>`);
|
|
577
|
+
if (skill.license !== void 0) lines.push(` <license>${escapeXml(skill.license)}</license>`);
|
|
578
|
+
lines.push(` <source>${escapeXml(skill.source)}</source>`);
|
|
579
|
+
lines.push(" </skill>");
|
|
580
|
+
}
|
|
581
|
+
lines.push("</available_skills>");
|
|
582
|
+
return lines.join("\n");
|
|
583
|
+
}
|
|
584
|
+
const xmlRenderer = {
|
|
585
|
+
format: "xml",
|
|
586
|
+
render: renderAvailableSkillsXml
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
//#endregion
|
|
590
|
+
//#region ../runtime/dist/memoryArtifactStore-C9lFVqPF.js
|
|
591
|
+
/** 内存 ArtifactStore:测试与浏览器降级用;索引随进程生命周期存在 */
|
|
592
|
+
var MemoryArtifactStore = class {
|
|
593
|
+
#byRun = /* @__PURE__ */ new Map();
|
|
594
|
+
#seq = 0;
|
|
595
|
+
async createTextArtifact(input) {
|
|
596
|
+
return this.#record({
|
|
597
|
+
runId: input.runId,
|
|
598
|
+
path: input.path,
|
|
599
|
+
type: "text",
|
|
600
|
+
size: new TextEncoder().encode(input.content).length,
|
|
601
|
+
mimeType: input.mimeType,
|
|
602
|
+
metadata: input.metadata
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
async createBinaryArtifact(input) {
|
|
606
|
+
return this.#record({
|
|
607
|
+
runId: input.runId,
|
|
608
|
+
path: input.path,
|
|
609
|
+
type: "binary",
|
|
610
|
+
size: input.content.length,
|
|
611
|
+
mimeType: input.mimeType,
|
|
612
|
+
metadata: input.metadata
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async listArtifacts(runId) {
|
|
616
|
+
return [...this.#byRun.get(runId) ?? []];
|
|
617
|
+
}
|
|
618
|
+
#record(input) {
|
|
619
|
+
const artifact = {
|
|
620
|
+
id: `art-${++this.#seq}`,
|
|
621
|
+
runId: input.runId,
|
|
622
|
+
path: input.path,
|
|
623
|
+
type: input.type,
|
|
624
|
+
mimeType: input.mimeType,
|
|
625
|
+
size: input.size,
|
|
626
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
627
|
+
metadata: input.metadata
|
|
628
|
+
};
|
|
629
|
+
const list = this.#byRun.get(input.runId) ?? [];
|
|
630
|
+
list.push(artifact);
|
|
631
|
+
this.#byRun.set(input.runId, list);
|
|
632
|
+
return artifact;
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
//#endregion
|
|
637
|
+
export { renderAvailableSkillsXml as C, verifyManifest as D, validateSkills as E, xmlRenderer as O, parseSkillPackManifest as S, resolveInsideRoot as T, exportSkills as _, SKILL_NAME_MAX_LENGTH as a, normalizePath as b, SkillDiscovery as c, buildCatalog as d, buildManifest as f, escapeXml as g, computeDigest as h, SKILL_MANIFEST_FILE as i, SkillReader as l, checkSkillRules as m, MemoryFS as n, SKILL_NAME_PATTERN as o, checkDependencyCycles as p, SKILLS_LOCKFILE as r, SKILL_PACK_FILE as s, MemoryArtifactStore as t, WebSkillError as u, isValidSkillName as v, renderCatalogJson as w, parseSkillMarkdown as x, jsonRenderer as y };
|
package/dist/node.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { E as SKILL_MANIFEST_FILE, H as SkillsLockfile, L as SkillManifest, P as SkillInstallSource, T as SKILLS_LOCKFILE, W as VerifyResult } from "./types-CgNC-oQu-Dq_A1yA-.js";
|
|
2
|
+
import { ct as createScriptContext } from "./index-oG5Nzgb9.js";
|
|
3
|
+
import { a as LlmEnvConfig, c as OxcSchemaInferer, d as SkillManager, f as loadLlmConfigFromEnv, i as LlmCapabilities, l as SandboxOptions, m as readArchiveManifest, n as FileArtifactStore, o as NodeFS, p as probeLlmCapabilities, r as FileMemoryStore, s as NodeScriptExecutor, t as CliUiBridge, u as SandboxedScriptExecutor } from "./index-Dc4X2N54.js";
|
|
4
|
+
export { CliUiBridge, FileArtifactStore, FileMemoryStore, type LlmCapabilities, type LlmEnvConfig, NodeFS, NodeScriptExecutor, OxcSchemaInferer, SKILLS_LOCKFILE, SKILL_MANIFEST_FILE, type SandboxOptions, SandboxedScriptExecutor, type SkillInstallSource, SkillManager, type SkillManifest, type SkillsLockfile, type VerifyResult, createScriptContext, loadLlmConfigFromEnv, probeLlmCapabilities, readArchiveManifest };
|
package/dist/node.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { i as SKILL_MANIFEST_FILE, r as SKILLS_LOCKFILE } from "./memoryArtifactStore-C9lFVqPF-U8nXvqL9.js";
|
|
2
|
+
import { S as createScriptContext } from "./dist-D5fsiETF.js";
|
|
3
|
+
import { a as NodeScriptExecutor, c as SkillManager, d as readArchiveManifest, i as NodeFS, l as loadLlmConfigFromEnv, n as FileArtifactStore, o as OxcSchemaInferer, r as FileMemoryStore, s as SandboxedScriptExecutor, t as CliUiBridge, u as probeLlmCapabilities } from "./dist-Cc3Mqi3_.js";
|
|
3
4
|
|
|
4
5
|
export { CliUiBridge, FileArtifactStore, FileMemoryStore, NodeFS, NodeScriptExecutor, OxcSchemaInferer, SKILLS_LOCKFILE, SKILL_MANIFEST_FILE, SandboxedScriptExecutor, SkillManager, createScriptContext, loadLlmConfigFromEnv, probeLlmCapabilities, readArchiveManifest };
|