@webskill/sdk 0.0.4 → 0.0.6

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.
@@ -1,4 +1,5 @@
1
1
  import { parse } from "yaml";
2
+ import { zipSync } from "fflate";
2
3
 
3
4
  //#region ../core/dist/index.js
4
5
  /** 所有公开 API 抛出的结构化错误,code 供上层可编程处理 */
@@ -259,8 +260,137 @@ function checkSkillRules(input) {
259
260
  severity: "error",
260
261
  message: `Unsupported script file in scripts/ of directory ${dirName}: ${fileName} (only .ts/.js and .schema.json sidecars allowed)`
261
262
  });
263
+ const allowedTools = metadata?.["allowed-tools"];
264
+ if (Array.isArray(allowedTools)) {
265
+ const scriptNames = new Set((input.scriptFileNames ?? []).filter((f) => SCRIPT_EXTENSION_RE.test(f)).map((f) => f.replace(SCRIPT_EXTENSION_RE, "")));
266
+ for (const entry of allowedTools) if (typeof entry === "string" && !scriptNames.has(entry)) issues.push({
267
+ code: "SKILL_UNKNOWN_ALLOWED_TOOL",
268
+ severity: "warning",
269
+ message: `allowed-tools entry ${JSON.stringify(entry)} of directory ${dirName} does not match any script in scripts/`
270
+ });
271
+ }
272
+ if (metadata && input.knownSkillNames) {
273
+ const dependencies = metadata["dependencies"];
274
+ if (Array.isArray(dependencies)) {
275
+ for (const dep of dependencies) if (typeof dep === "string" && !input.knownSkillNames.has(dep)) issues.push({
276
+ code: "SKILL_UNKNOWN_DEPENDENCY",
277
+ severity: "error",
278
+ message: `Skill ${JSON.stringify(metadata.name)} depends on unknown skill ${JSON.stringify(dep)}`
279
+ });
280
+ }
281
+ }
262
282
  return issues;
263
283
  }
284
+ /**
285
+ * 循环依赖检测(全量邻接表,discovery 两趟扫描第二趟调用)。
286
+ * 每个环只报一次(error 级,阻断进 Catalog);自引用视为长度 1 的环。
287
+ */
288
+ function checkDependencyCycles(adjacency) {
289
+ const issues = [];
290
+ const involved = /* @__PURE__ */ new Set();
291
+ const reported = /* @__PURE__ */ new Set();
292
+ const state = /* @__PURE__ */ new Map();
293
+ const stack = [];
294
+ const visit = (node) => {
295
+ if (state.get(node) === "done") return;
296
+ if (state.get(node) === "visiting") {
297
+ const cycle = [...stack.slice(stack.indexOf(node)), node];
298
+ const key = cycle.slice(0, -1).sort().join("");
299
+ if (!reported.has(key)) {
300
+ reported.add(key);
301
+ for (const name of cycle.slice(0, -1)) involved.add(name);
302
+ issues.push({
303
+ code: "SKILL_CIRCULAR_DEPENDENCY",
304
+ severity: "error",
305
+ message: `Circular skill dependency detected: ${cycle.join(" -> ")}`
306
+ });
307
+ }
308
+ return;
309
+ }
310
+ state.set(node, "visiting");
311
+ stack.push(node);
312
+ for (const dep of adjacency.get(node) ?? []) visit(dep);
313
+ stack.pop();
314
+ state.set(node, "done");
315
+ };
316
+ for (const node of adjacency.keys()) visit(node);
317
+ return {
318
+ issues,
319
+ involved
320
+ };
321
+ }
322
+ /**
323
+ * 技能包集(skill pack)格式与打包逻辑(node/browser 共用单一来源):
324
+ * webskill.skill-pack.json { schemaVersion: 1, skills: [{ name, digest }] }
325
+ * <skill-a>/SKILL.md … webskill.skill-manifest.json
326
+ * <skill-b>/SKILL.md …
327
+ * fflate 环境无关;文件读取经 FileSystemProvider 抽象。
328
+ */
329
+ /** 包级清单文件名(位于 zip 根,不参与各技能 digest) */
330
+ const SKILL_PACK_FILE = "webskill.skill-pack.json";
331
+ const messageOf$2 = (e) => e instanceof Error ? e.message : String(e);
332
+ const baseName$1 = (p) => p.split("/").pop() ?? p;
333
+ async function listFilesRecursive(fs, root, prefix = "") {
334
+ const out = [];
335
+ for (const entry of await fs.list(root)) {
336
+ const rel = prefix === "" ? baseName$1(entry.path) : `${prefix}/${baseName$1(entry.path)}`;
337
+ if (entry.type === "directory") out.push(...await listFilesRecursive(fs, entry.path, rel));
338
+ else out.push(rel);
339
+ }
340
+ return out;
341
+ }
342
+ /**
343
+ * 多技能打包为单一 zip(包级清单 + 各技能目录含自身 manifest)。
344
+ * manifestBuilder 负责确保技能目录内 manifest 文件存在并返回之(digest 入包级清单)。
345
+ */
346
+ async function exportSkills(fs, input) {
347
+ if (input.roots.length === 0) throw new WebSkillError("EXPORT_FAILED", "exportSkills requires at least one skill root");
348
+ const files = {};
349
+ const skills = [];
350
+ try {
351
+ for (const root of input.roots) {
352
+ const base = root.replace(/\/+$/, "");
353
+ const manifest = await input.manifestBuilder(base);
354
+ skills.push({
355
+ name: manifest.name,
356
+ digest: manifest.integrity.digest
357
+ });
358
+ for (const rel of await listFilesRecursive(fs, base)) files[`${manifest.name}/${rel}`] = await fs.readBinary(`${base}/${rel}`);
359
+ }
360
+ } catch (e) {
361
+ if (e instanceof WebSkillError) throw e;
362
+ throw new WebSkillError("EXPORT_FAILED", `Failed to export skill pack: ${messageOf$2(e)}`, e);
363
+ }
364
+ const pack = {
365
+ schemaVersion: 1,
366
+ skills
367
+ };
368
+ files[SKILL_PACK_FILE] = new TextEncoder().encode(JSON.stringify(pack, null, 2));
369
+ return zipSync(files, { level: 6 });
370
+ }
371
+ /** 包级清单解析 + 形状校验;损坏 → INSTALL_FAILED(安装侧保证零残留) */
372
+ function parseSkillPackManifest(text) {
373
+ let raw;
374
+ try {
375
+ raw = JSON.parse(text);
376
+ } catch (e) {
377
+ throw new WebSkillError("INSTALL_FAILED", `Skill pack manifest is not valid JSON: ${messageOf$2(e)}`, e);
378
+ }
379
+ const invalid = (detail) => {
380
+ throw new WebSkillError("INSTALL_FAILED", `Skill pack manifest is corrupted: ${detail}`);
381
+ };
382
+ if (typeof raw !== "object" || raw === null) invalid("not an object");
383
+ const record = raw;
384
+ if (record["schemaVersion"] !== 1) invalid("schemaVersion must be 1");
385
+ if (!Array.isArray(record["skills"]) || record["skills"].length === 0) invalid("\"skills\" must be a non-empty array");
386
+ for (const entry of record["skills"]) {
387
+ if (typeof entry !== "object" || entry === null) invalid("skill entry is not an object");
388
+ const { name, digest } = entry;
389
+ if (typeof name !== "string" || name === "") invalid("skill entry missing a valid \"name\"");
390
+ if (typeof digest !== "string" || digest === "") invalid(`skill "${name}" missing a valid "digest"`);
391
+ }
392
+ return raw;
393
+ }
264
394
  /** 由条目列表构建 Catalog,保证按名称排序 */
265
395
  function buildCatalog(entries) {
266
396
  return { skills: [...entries].sort((a, b) => a.name.localeCompare(b.name)) };
@@ -288,7 +418,7 @@ var SkillReader = class {
288
418
  return this.#fs.readBinary(resolveInsideRoot(this.#rootFor(skillName), relativePath));
289
419
  }
290
420
  };
291
- const baseName$1 = (path) => path.split("/").pop() ?? path;
421
+ const baseName$2 = (path) => path.split("/").pop() ?? path;
292
422
  var SkillDiscovery = class {
293
423
  #fs;
294
424
  #roots;
@@ -304,7 +434,9 @@ var SkillDiscovery = class {
304
434
  this.#index.clear();
305
435
  const entries = [];
306
436
  const issues = [];
307
- const claimedNames = /* @__PURE__ */ new Set();
437
+ const candidates = [];
438
+ const knownSkillNames = /* @__PURE__ */ new Set();
439
+ const adjacency = /* @__PURE__ */ new Map();
308
440
  for (const root of this.#roots) {
309
441
  if (!await this.#fs.exists(root)) {
310
442
  issues.push({
@@ -317,7 +449,7 @@ var SkillDiscovery = class {
317
449
  }
318
450
  for (const child of await this.#fs.list(root)) {
319
451
  if (child.type !== "directory") continue;
320
- const dirName = baseName$1(child.path);
452
+ const dirName = baseName$2(child.path);
321
453
  const skillRoot = `${root}/${dirName}`;
322
454
  const skillMdPath = `${skillRoot}/SKILL.md`;
323
455
  const hasSkillMd = await this.#fs.exists(skillMdPath);
@@ -328,33 +460,53 @@ var SkillDiscovery = class {
328
460
  } catch (e) {
329
461
  parseError = e instanceof WebSkillError ? e : new WebSkillError("SKILL_INVALID_METADATA", String(e));
330
462
  }
331
- const scriptFileNames = await this.#listScriptFileNames(skillRoot);
332
- const candidateIssues = checkSkillRules({
463
+ candidates.push({
333
464
  dirName,
465
+ skillRoot,
334
466
  hasSkillMd,
335
467
  metadata,
336
468
  parseError,
337
- scriptFileNames,
338
- existingNames: claimedNames
339
- });
340
- for (const issue of candidateIssues) issue.path ??= skillRoot;
341
- issues.push(...candidateIssues);
342
- if (metadata) claimedNames.add(metadata.name);
343
- if (candidateIssues.some((i) => i.severity === "error") || !metadata) continue;
344
- this.#index.set(metadata.name, skillRoot);
345
- entries.push({
346
- name: metadata.name,
347
- description: metadata.description,
348
- version: typeof metadata["version"] === "string" ? metadata["version"] : void 0,
349
- license: typeof metadata["license"] === "string" ? metadata["license"] : void 0,
350
- root: skillRoot,
351
- source: "local",
352
- hasScripts: scriptFileNames.length > 0,
353
- hasReferences: await this.#fs.exists(`${skillRoot}/references`),
354
- hasAssets: await this.#fs.exists(`${skillRoot}/assets`)
469
+ scriptFileNames: await this.#listScriptFileNames(skillRoot)
355
470
  });
471
+ if (metadata) {
472
+ knownSkillNames.add(metadata.name);
473
+ const dependencies = metadata["dependencies"];
474
+ adjacency.set(metadata.name, Array.isArray(dependencies) ? dependencies.filter((d) => typeof d === "string") : []);
475
+ }
356
476
  }
357
477
  }
478
+ const cycles = checkDependencyCycles(adjacency);
479
+ const claimedNames = /* @__PURE__ */ new Set();
480
+ for (const candidate of candidates) {
481
+ const { metadata } = candidate;
482
+ const candidateIssues = checkSkillRules({
483
+ dirName: candidate.dirName,
484
+ hasSkillMd: candidate.hasSkillMd,
485
+ metadata,
486
+ parseError: candidate.parseError,
487
+ scriptFileNames: candidate.scriptFileNames,
488
+ existingNames: claimedNames,
489
+ knownSkillNames
490
+ });
491
+ for (const issue of candidateIssues) issue.path ??= candidate.skillRoot;
492
+ issues.push(...candidateIssues);
493
+ if (metadata) claimedNames.add(metadata.name);
494
+ const excludedByCycle = metadata !== void 0 && cycles.involved.has(metadata.name);
495
+ if (candidateIssues.some((i) => i.severity === "error") || !metadata || excludedByCycle) continue;
496
+ this.#index.set(metadata.name, candidate.skillRoot);
497
+ entries.push({
498
+ name: metadata.name,
499
+ description: metadata.description,
500
+ version: typeof metadata["version"] === "string" ? metadata["version"] : void 0,
501
+ license: typeof metadata["license"] === "string" ? metadata["license"] : void 0,
502
+ root: candidate.skillRoot,
503
+ source: "local",
504
+ hasScripts: candidate.scriptFileNames.length > 0,
505
+ hasReferences: await this.#fs.exists(`${candidate.skillRoot}/references`),
506
+ hasAssets: await this.#fs.exists(`${candidate.skillRoot}/assets`)
507
+ });
508
+ }
509
+ issues.push(...cycles.issues);
358
510
  entries.sort((a, b) => a.name.localeCompare(b.name));
359
511
  return {
360
512
  entries,
@@ -386,7 +538,7 @@ var SkillDiscovery = class {
386
538
  async #listScriptFileNames(skillRoot) {
387
539
  const scriptsDir = `${skillRoot}/scripts`;
388
540
  if (!await this.#fs.exists(scriptsDir)) return [];
389
- return (await this.#fs.list(scriptsDir)).filter((s) => s.type === "file").map((s) => baseName$1(s.path));
541
+ return (await this.#fs.list(scriptsDir)).filter((s) => s.type === "file").map((s) => baseName$2(s.path));
390
542
  }
391
543
  };
392
544
  /**
@@ -908,7 +1060,7 @@ const ASK_USER_TOOL = {
908
1060
  * 不暴露 fs 本体(沙箱语义,对齐 deferred-items D1)。
909
1061
  */
910
1062
  function createScriptContext(deps) {
911
- const { fs, artifactStore, skillName, skillRoot, runId, confirm } = deps;
1063
+ const { fs, artifactStore, skillName, skillRoot, runId, confirm, onWarning } = deps;
912
1064
  return {
913
1065
  skillName,
914
1066
  runId,
@@ -928,15 +1080,46 @@ function createScriptContext(deps) {
928
1080
  mimeType: options?.mimeType
929
1081
  });
930
1082
  },
931
- ...confirm ? { confirm } : {}
1083
+ ...confirm ? { confirm } : {},
1084
+ ...onWarning ? { onWarning } : {}
1085
+ };
1086
+ }
1087
+ const isRecord$1 = (v) => typeof v === "object" && v !== null;
1088
+ /**
1089
+ * $chart 约定的形状校验:JSON content 的 data 含 $chart 键且形状合法 → ChartSpec;
1090
+ * 任何畸形(kind 非法 / labels 非字符串数组 / series 项缺数值 data)→ undefined(忽略不炸)。
1091
+ */
1092
+ function extractChartSpec(data) {
1093
+ if (!isRecord$1(data)) return void 0;
1094
+ const raw = data["$chart"];
1095
+ if (!isRecord$1(raw)) return void 0;
1096
+ const { kind, labels, series } = raw;
1097
+ if (kind !== "bar" && kind !== "line" && kind !== "pie") return void 0;
1098
+ if (!Array.isArray(labels) || !labels.every((l) => typeof l === "string")) return void 0;
1099
+ if (!Array.isArray(series)) return void 0;
1100
+ const validSeries = [];
1101
+ for (const item of series) {
1102
+ if (!isRecord$1(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
1103
+ const name = item["name"];
1104
+ validSeries.push({
1105
+ ...typeof name === "string" ? { name } : {},
1106
+ data: item["data"]
1107
+ });
1108
+ }
1109
+ const title = raw["title"];
1110
+ return {
1111
+ kind,
1112
+ labels,
1113
+ series: validSeries,
1114
+ ...typeof title === "string" ? { title } : {}
932
1115
  };
933
1116
  }
934
1117
  /**
935
- * 默认的结果渲染构造:LLM 最终输出 markdown block,
936
- * run.artifacts → file blockssummary 取 terminationReason。
1118
+ * 默认的结果渲染构造:run 内收集的 renderBlocks(chart 等)在前,
1119
+ * LLM 最终输出 → markdown block,run.artifacts → file blocks 在后;summary 取 terminationReason。
937
1120
  */
938
- function buildRenderResult(run, output) {
939
- const blocks = [];
1121
+ function buildRenderResult(run, output, renderBlocks = []) {
1122
+ const blocks = [...renderBlocks];
940
1123
  if (output.trim() !== "") blocks.push({
941
1124
  type: "markdown",
942
1125
  text: output
@@ -1043,131 +1226,6 @@ var EventBus = class {
1043
1226
  for (const listener of this.#listeners.get("*") ?? []) listener(frozen);
1044
1227
  }
1045
1228
  };
1046
- const messageOf$1 = (e) => e instanceof Error ? e.message : String(e);
1047
- /** 受控钩子执行器:逐个执行,超时/异常默认降级为 warning,可切严格模式 */
1048
- var HookRunner = class {
1049
- #hooks = /* @__PURE__ */ new Map();
1050
- timeoutMs;
1051
- failOnHookError;
1052
- onWarning;
1053
- constructor(options = {}) {
1054
- this.timeoutMs = options.timeoutMs ?? 5e3;
1055
- this.failOnHookError = options.failOnHookError ?? false;
1056
- this.onWarning = options.onWarning;
1057
- }
1058
- register(phase, hook) {
1059
- const key = phase;
1060
- const list = this.#hooks.get(key) ?? [];
1061
- list.push(hook);
1062
- this.#hooks.set(key, list);
1063
- return this;
1064
- }
1065
- async run(phase, ctx) {
1066
- const hooks = [...this.#hooks.get(phase) ?? [], ...this.#hooks.get("*") ?? []];
1067
- for (const hook of hooks) try {
1068
- await this.#withTimeout(Promise.resolve().then(() => hook(ctx)), phase);
1069
- } catch (e) {
1070
- if (this.failOnHookError) throw e instanceof WebSkillError ? e : new WebSkillError("RUN_FAILED", messageOf$1(e), e);
1071
- this.onWarning?.(messageOf$1(e));
1072
- }
1073
- }
1074
- #withTimeout(promise, phase) {
1075
- return new Promise((resolve, reject) => {
1076
- const timer = setTimeout(() => reject(new WebSkillError("RUN_TIMEOUT", `Lifecycle hook for phase "${phase}" timed out after ${this.timeoutMs}ms`)), this.timeoutMs);
1077
- promise.then((v) => {
1078
- clearTimeout(timer);
1079
- resolve(v);
1080
- }, (e) => {
1081
- clearTimeout(timer);
1082
- reject(e instanceof Error ? e : new Error(String(e)));
1083
- });
1084
- });
1085
- }
1086
- };
1087
- /** 内存 MemoryStore:测试与浏览器降级用 */
1088
- var InMemoryStore = class {
1089
- #data = /* @__PURE__ */ new Map();
1090
- async get(scope, key) {
1091
- return this.#data.get(scope)?.get(key);
1092
- }
1093
- async set(scope, key, value) {
1094
- let bucket = this.#data.get(scope);
1095
- if (!bucket) {
1096
- bucket = /* @__PURE__ */ new Map();
1097
- this.#data.set(scope, bucket);
1098
- }
1099
- bucket.set(key, value);
1100
- }
1101
- async delete(scope, key) {
1102
- this.#data.get(scope)?.delete(key);
1103
- }
1104
- async list(scope) {
1105
- return [...(this.#data.get(scope) ?? /* @__PURE__ */ new Map()).entries()].map(([key, value]) => ({
1106
- key,
1107
- value
1108
- }));
1109
- }
1110
- async clear(scope) {
1111
- if (scope === void 0) this.#data.clear();
1112
- else this.#data.delete(scope);
1113
- }
1114
- };
1115
- const encode = (s) => encodeURIComponent(s);
1116
- /**
1117
- * 基于 FileSystemProvider 的 MemoryStore:
1118
- * <root>/<encodeURIComponent(scope)>/<encodeURIComponent(key)>.json
1119
- * scope/key 全部编码防路径穿越;环境无关,构造必须显式传 fs。
1120
- */
1121
- var FsMemoryStore = class {
1122
- #root;
1123
- #fs;
1124
- constructor(deps) {
1125
- this.#root = deps.root.replace(/\/+$/, "");
1126
- this.#fs = deps.fs;
1127
- }
1128
- #scopeDir(scope) {
1129
- return `${this.#root}/${encode(scope)}`;
1130
- }
1131
- #keyPath(scope, key) {
1132
- return `${this.#scopeDir(scope)}/${encode(key)}.json`;
1133
- }
1134
- async get(scope, key) {
1135
- const path = this.#keyPath(scope, key);
1136
- if (!await this.#fs.exists(path)) return void 0;
1137
- return JSON.parse(await this.#fs.readText(path));
1138
- }
1139
- async set(scope, key, value) {
1140
- await this.#fs.writeText(this.#keyPath(scope, key), JSON.stringify(value, null, 2));
1141
- }
1142
- async delete(scope, key) {
1143
- const path = this.#keyPath(scope, key);
1144
- if (await this.#fs.exists(path)) await this.#fs.remove(path);
1145
- }
1146
- async list(scope) {
1147
- const dir = this.#scopeDir(scope);
1148
- if (!await this.#fs.exists(dir)) return [];
1149
- const out = [];
1150
- for (const entry of await this.#fs.list(dir)) {
1151
- if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
1152
- const fileName = entry.path.split("/").pop() ?? "";
1153
- const key = decodeURIComponent(fileName.replace(/\.json$/, ""));
1154
- out.push({
1155
- key,
1156
- value: JSON.parse(await this.#fs.readText(entry.path))
1157
- });
1158
- }
1159
- return out.sort((a, b) => a.key.localeCompare(b.key));
1160
- }
1161
- async clear(scope) {
1162
- if (scope !== void 0) {
1163
- const dir = this.#scopeDir(scope);
1164
- if (await this.#fs.exists(dir)) await this.#fs.remove(dir, { recursive: true });
1165
- return;
1166
- }
1167
- if (!await this.#fs.exists(this.#root)) return;
1168
- for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
1169
- }
1170
- };
1171
1229
  /** 内存 ArtifactStore:测试与浏览器降级用;索引随进程生命周期存在 */
1172
1230
  var MemoryArtifactStore = class {
1173
1231
  #byRun = /* @__PURE__ */ new Map();
@@ -1212,142 +1270,44 @@ var MemoryArtifactStore = class {
1212
1270
  return artifact;
1213
1271
  }
1214
1272
  };
1215
- const INDEX_FILE = "index.json";
1216
- /**
1217
- * 基于 FileSystemProvider 的 ArtifactStore:产物落盘 <root>/<runId>/<path>,
1218
- * 每次写入同步更新 <root>/<runId>/index.json,新实例可凭索引恢复列表。
1219
- * 环境无关(OPFS/Node 均可),构造必须显式传 fs。
1220
- */
1221
- var FsArtifactStore = class {
1222
- #root;
1223
- #fs;
1224
- constructor(deps) {
1225
- this.#root = deps.root.replace(/\/+$/, "");
1226
- this.#fs = deps.fs;
1227
- }
1228
- async createTextArtifact(input) {
1229
- const size = new TextEncoder().encode(input.content).length;
1230
- await this.#fs.writeText(this.#artifactPath(input.runId, input.path), input.content);
1231
- return this.#persist({
1232
- ...input,
1233
- type: "text",
1234
- size
1235
- });
1236
- }
1237
- async createBinaryArtifact(input) {
1238
- await this.#fs.writeBinary(this.#artifactPath(input.runId, input.path), input.content);
1239
- return this.#persist({
1240
- ...input,
1241
- type: "binary",
1242
- size: input.content.length
1243
- });
1273
+ /** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
1274
+ var TraceRecorder = class {
1275
+ #runId;
1276
+ #clock;
1277
+ #events = [];
1278
+ #seq = 0;
1279
+ constructor(runId, clock = {}) {
1280
+ this.#runId = runId;
1281
+ this.#clock = clock;
1244
1282
  }
1245
- async listArtifacts(runId) {
1246
- const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
1247
- if (!await this.#fs.exists(indexPath)) return [];
1248
- const raw = await this.#fs.readText(indexPath);
1249
- return JSON.parse(raw).artifacts ?? [];
1283
+ record(type, opts) {
1284
+ const event = {
1285
+ id: this.#clock.createId?.() ?? `evt-${++this.#seq}`,
1286
+ runId: this.#runId,
1287
+ ts: this.#clock.now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
1288
+ type,
1289
+ ...opts?.message !== void 0 ? { message: opts.message } : {},
1290
+ ...opts?.data !== void 0 ? { data: opts.data } : {}
1291
+ };
1292
+ this.#events.push(event);
1293
+ return event;
1250
1294
  }
1251
- #artifactPath(runId, artifactPath) {
1252
- return resolveInsideRoot(`${this.#root}/${runId}`, artifactPath);
1295
+ list() {
1296
+ return [...this.#events];
1253
1297
  }
1254
- async #persist(input) {
1255
- const artifact = {
1256
- id: `${input.runId}/${input.path}`,
1257
- runId: input.runId,
1258
- path: input.path,
1259
- type: input.type,
1260
- mimeType: input.mimeType,
1261
- size: input.size,
1262
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1263
- metadata: input.metadata
1264
- };
1265
- const next = [...(await this.listArtifacts(input.runId)).filter((a) => a.id !== artifact.id), artifact];
1266
- await this.#fs.writeText(`${this.#root}/${input.runId}/${INDEX_FILE}`, JSON.stringify({ artifacts: next }, null, 2));
1267
- return artifact;
1268
- }
1269
- };
1270
- const isRecord = (v) => typeof v === "object" && v !== null;
1271
- const isNonEmptyString = (v) => typeof v === "string" && v !== "";
1272
- /** 入站校验:非法 kind / 缺 id / 缺字段 → undefined(调用方忽略并记 warning) */
1273
- function parseBridgeRequest(data) {
1274
- if (!isRecord(data)) return void 0;
1275
- const { kind, id } = data;
1276
- if (!isNonEmptyString(id)) return void 0;
1277
- switch (kind) {
1278
- case "readReference": return isNonEmptyString(data["path"]) ? {
1279
- kind,
1280
- id,
1281
- path: data["path"]
1282
- } : void 0;
1283
- case "writeArtifact": {
1284
- const { path, content, mimeType } = data;
1285
- const validContent = typeof content === "string" || Array.isArray(content) && content.every((n) => typeof n === "number");
1286
- if (!isNonEmptyString(path) || !validContent) return void 0;
1287
- return {
1288
- kind,
1289
- id,
1290
- path,
1291
- content,
1292
- ...isNonEmptyString(mimeType) ? { mimeType } : {}
1293
- };
1294
- }
1295
- case "confirm": return isNonEmptyString(data["message"]) ? {
1296
- kind,
1297
- id,
1298
- message: data["message"]
1299
- } : void 0;
1300
- default: return;
1301
- }
1302
- }
1303
- function bridgeError(id, code, message) {
1304
- return {
1305
- id,
1306
- ok: false,
1307
- error: {
1308
- code,
1309
- message
1310
- }
1311
- };
1312
- }
1313
- /** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
1314
- var TraceRecorder = class {
1315
- #runId;
1316
- #clock;
1317
- #events = [];
1318
- #seq = 0;
1319
- constructor(runId, clock = {}) {
1320
- this.#runId = runId;
1321
- this.#clock = clock;
1322
- }
1323
- record(type, opts) {
1324
- const event = {
1325
- id: this.#clock.createId?.() ?? `evt-${++this.#seq}`,
1326
- runId: this.#runId,
1327
- ts: this.#clock.now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
1328
- type,
1329
- ...opts?.message !== void 0 ? { message: opts.message } : {},
1330
- ...opts?.data !== void 0 ? { data: opts.data } : {}
1331
- };
1332
- this.#events.push(event);
1333
- return event;
1334
- }
1335
- list() {
1336
- return [...this.#events];
1337
- }
1338
- };
1339
- /** 交互终态(取消/超时):从工具执行深处直接终止 run */
1340
- var RunTerminated = class extends Error {
1341
- outcome;
1342
- constructor(outcome) {
1343
- super(outcome.message);
1344
- this.outcome = outcome;
1298
+ };
1299
+ /** 交互终态(取消/超时):从工具执行深处直接终止 run */
1300
+ var RunTerminated = class extends Error {
1301
+ outcome;
1302
+ constructor(outcome) {
1303
+ super(outcome.message);
1304
+ this.outcome = outcome;
1345
1305
  }
1346
1306
  };
1347
1307
  /** bridge.request 自身异常(非超时):恢复 running 后转为工具错误回喂 */
1348
1308
  var BridgeRequestError = class extends Error {};
1349
1309
  const baseName = (p) => p.split("/").pop() ?? p;
1350
- const messageOf = (e) => e instanceof Error ? e.message : String(e);
1310
+ const messageOf$1 = (e) => e instanceof Error ? e.message : String(e);
1351
1311
  const toolError = (code, message) => ({
1352
1312
  ok: false,
1353
1313
  content: [],
@@ -1412,7 +1372,8 @@ var AgentLoop = class {
1412
1372
  now,
1413
1373
  interactionSeq: 0,
1414
1374
  messages: [],
1415
- turn: 0
1375
+ turn: 0,
1376
+ renderBlocks: []
1416
1377
  };
1417
1378
  if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
1418
1379
  const route = this.#deps.catalogFilter ? {
@@ -1428,7 +1389,7 @@ var AgentLoop = class {
1428
1389
  try {
1429
1390
  return await source.listToolSpecs();
1430
1391
  } catch (e) {
1431
- trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
1392
+ trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf$1(e)}` });
1432
1393
  return [];
1433
1394
  }
1434
1395
  }))).flat();
@@ -1442,7 +1403,7 @@ var AgentLoop = class {
1442
1403
  try {
1443
1404
  return await this.#turnLoop(state, startMs, 1, externalSpecs);
1444
1405
  } catch (e) {
1445
- if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf(e), "RUN_FAILED");
1406
+ if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf$1(e), "RUN_FAILED");
1446
1407
  throw e;
1447
1408
  }
1448
1409
  }
@@ -1481,7 +1442,7 @@ var AgentLoop = class {
1481
1442
  });
1482
1443
  } catch (e) {
1483
1444
  const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
1484
- return finish("failed", "llm-error", messageOf(e), code);
1445
+ return finish("failed", "llm-error", messageOf$1(e), code);
1485
1446
  }
1486
1447
  trace.record("llm.response", { data: {
1487
1448
  turn,
@@ -1533,21 +1494,21 @@ var AgentLoop = class {
1533
1494
  });
1534
1495
  const bridge = this.#deps.uiBridge;
1535
1496
  if (status === "completed" && bridge?.renderResult && this.#config.renderResult !== "off") try {
1536
- const request = buildRenderResult(run, output);
1497
+ const request = buildRenderResult(run, output, state.renderBlocks);
1537
1498
  await bridge.renderResult(request);
1538
1499
  trace.record("ui.rendered", { data: { blockCount: request.blocks.length } });
1539
1500
  } catch (e) {
1540
- trace.record("run.warning", { message: `renderResult failed: ${messageOf(e)}` });
1501
+ trace.record("run.warning", { message: `renderResult failed: ${messageOf$1(e)}` });
1541
1502
  }
1542
1503
  if (this.#deps.snapshotStore) try {
1543
1504
  await this.#deps.snapshotStore.delete(state.runId);
1544
1505
  } catch (e) {
1545
- trace.record("run.warning", { message: `Failed to delete run snapshot: ${messageOf(e)}` });
1506
+ trace.record("run.warning", { message: `Failed to delete run snapshot: ${messageOf$1(e)}` });
1546
1507
  }
1547
1508
  try {
1548
1509
  await this.#lifecycle(status === "completed" ? "complete" : "fail", state, { reason });
1549
1510
  } catch (e) {
1550
- trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
1511
+ trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf$1(e)}` });
1551
1512
  }
1552
1513
  run.trace = trace.list();
1553
1514
  return {
@@ -1582,7 +1543,7 @@ var AgentLoop = class {
1582
1543
  try {
1583
1544
  await store.save(snapshot);
1584
1545
  } catch (e) {
1585
- state.trace.record("run.warning", { message: `Failed to save run snapshot: ${messageOf(e)}` });
1546
+ state.trace.record("run.warning", { message: `Failed to save run snapshot: ${messageOf$1(e)}` });
1586
1547
  }
1587
1548
  }
1588
1549
  /**
@@ -1615,7 +1576,8 @@ var AgentLoop = class {
1615
1576
  now,
1616
1577
  interactionSeq: 0,
1617
1578
  messages: snapshot.messages.map((m) => ({ ...m })),
1618
- turn: snapshot.turn
1579
+ turn: snapshot.turn,
1580
+ renderBlocks: []
1619
1581
  };
1620
1582
  if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
1621
1583
  trace.record("run.resumed", { data: {
@@ -1681,14 +1643,14 @@ var AgentLoop = class {
1681
1643
  try {
1682
1644
  return await source.listToolSpecs();
1683
1645
  } catch (e) {
1684
- trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
1646
+ trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf$1(e)}` });
1685
1647
  return [];
1686
1648
  }
1687
1649
  }))).flat();
1688
1650
  try {
1689
1651
  return await this.#turnLoop(state, startMs, snapshot.turn + 1, externalSpecs);
1690
1652
  } catch (e) {
1691
- if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf(e), "RUN_FAILED");
1653
+ if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf$1(e), "RUN_FAILED");
1692
1654
  throw e;
1693
1655
  }
1694
1656
  }
@@ -1780,8 +1742,8 @@ var AgentLoop = class {
1780
1742
  message: e.message,
1781
1743
  code: "RUN_INTERACTION_TIMEOUT"
1782
1744
  });
1783
- state.trace.record("run.warning", { message: `UiBridge request failed: ${messageOf(e)}` });
1784
- throw new BridgeRequestError(messageOf(e));
1745
+ state.trace.record("run.warning", { message: `UiBridge request failed: ${messageOf$1(e)}` });
1746
+ throw new BridgeRequestError(messageOf$1(e));
1785
1747
  }
1786
1748
  if (response.cancelled) throw new RunTerminated({
1787
1749
  status: "cancelled",
@@ -1827,11 +1789,20 @@ var AgentLoop = class {
1827
1789
  result = source ? await source.call(call.name, call.arguments) : toolError(resolution.code, resolution.message);
1828
1790
  }
1829
1791
  }
1830
- if (result.ok) state.trace.record("tool.completed", { data: {
1831
- name: call.name,
1832
- callId: call.id
1833
- } });
1834
- else state.trace.record("tool.failed", {
1792
+ if (result.ok) {
1793
+ state.trace.record("tool.completed", { data: {
1794
+ name: call.name,
1795
+ callId: call.id
1796
+ } });
1797
+ for (const item of result.content) {
1798
+ if (item.type !== "json") continue;
1799
+ const chart = extractChartSpec(item.data);
1800
+ if (chart) state.renderBlocks.push({
1801
+ type: "chart",
1802
+ chart
1803
+ });
1804
+ }
1805
+ } else state.trace.record("tool.failed", {
1835
1806
  message: result.error?.message,
1836
1807
  data: {
1837
1808
  name: call.name,
@@ -1885,7 +1856,7 @@ var AgentLoop = class {
1885
1856
  };
1886
1857
  } catch (e) {
1887
1858
  if (e instanceof WebSkillError && e.code === "SKILL_NOT_FOUND" && (this.#deps.skillProviders?.length ?? 0) > 0) return this.#handleReadExternalSkill(skillName, pathArg, state);
1888
- return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `read_skill_file failed: ${messageOf(e)}`);
1859
+ return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `read_skill_file failed: ${messageOf$1(e)}`);
1889
1860
  }
1890
1861
  }
1891
1862
  /** 经外部技能提供者读取技能文档/资源;skillKey 为技能名或 mcp:// URI */
@@ -1928,34 +1899,58 @@ var AgentLoop = class {
1928
1899
  }
1929
1900
  return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}`);
1930
1901
  }
1931
- /** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用 */
1932
- async #activateSkill(skillName, state) {
1902
+ /** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用;dependencies 级联激活(via 记录来源) */
1903
+ async #activateSkill(skillName, state, via) {
1933
1904
  state.activated.add(skillName);
1934
- state.trace.record("skill.activated", { data: { skillName } });
1905
+ state.trace.record("skill.activated", { data: {
1906
+ skillName,
1907
+ ...via ? { via } : {}
1908
+ } });
1935
1909
  await this.#lifecycle("activate", state, { skillName });
1936
1910
  await this.#writeActivationMemory(skillName, state);
1937
1911
  const root = this.#deps.skillIndex.get(skillName);
1938
1912
  if (!root) return "";
1913
+ let allowedTools;
1914
+ let dependencies = [];
1915
+ try {
1916
+ const { metadata } = parseSkillMarkdown(await this.#deps.fs.readText(`${root}/SKILL.md`));
1917
+ const rawDeps = metadata["dependencies"];
1918
+ if (Array.isArray(rawDeps)) dependencies = rawDeps.filter((d) => typeof d === "string");
1919
+ const raw = metadata["allowed-tools"];
1920
+ if (raw !== void 0) if (Array.isArray(raw)) allowedTools = raw.filter((e) => typeof e === "string");
1921
+ else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "allowed-tools" metadata entry; ignored` });
1922
+ } catch {}
1939
1923
  const executor = this.#deps.executor;
1940
- if (!executor) return "";
1941
1924
  const loaded = [];
1942
- let scriptFiles = [];
1943
- try {
1944
- scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => baseName(s.path));
1945
- } catch {
1946
- return "";
1947
- }
1948
- for (const file of scriptFiles) {
1949
- const match = /^(.*)\.(ts|js)$/.exec(file);
1950
- if (!match?.[1]) continue;
1925
+ if (executor) {
1926
+ let scriptFiles = [];
1951
1927
  try {
1952
- const def = await executor.loadDefinition(root, match[1]);
1953
- await this.#enrichDefinition(root, match[1], def, state);
1954
- state.activatedTools.set(def.name, def);
1955
- loaded.push(def.name);
1956
- } catch {}
1928
+ scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => baseName(s.path));
1929
+ } catch {
1930
+ scriptFiles = [];
1931
+ }
1932
+ for (const file of scriptFiles) {
1933
+ const match = /^(.*)\.(ts|js)$/.exec(file);
1934
+ if (!match?.[1]) continue;
1935
+ if (allowedTools && !allowedTools.includes(match[1])) continue;
1936
+ try {
1937
+ const def = await executor.loadDefinition(root, match[1]);
1938
+ await this.#enrichDefinition(root, match[1], def, state);
1939
+ state.activatedTools.set(def.name, def);
1940
+ loaded.push(def.name);
1941
+ } catch {}
1942
+ }
1943
+ }
1944
+ let note = loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "";
1945
+ for (const dep of dependencies) {
1946
+ if (state.activated.has(dep)) continue;
1947
+ if (!this.#deps.skillIndex.has(dep)) {
1948
+ state.trace.record("run.warning", { message: `Skill "${skillName}" depends on unknown skill "${dep}"; skipped` });
1949
+ continue;
1950
+ }
1951
+ note += await this.#activateSkill(dep, state, skillName);
1957
1952
  }
1958
- return loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "";
1953
+ return note;
1959
1954
  }
1960
1955
  /**
1961
1956
  * D2 Schema 兜底链(同一 run 内激活时只算一次):
@@ -1968,7 +1963,7 @@ var AgentLoop = class {
1968
1963
  def.inputSchema = JSON.parse(await this.#deps.fs.readText(sidecar));
1969
1964
  return;
1970
1965
  } catch (e) {
1971
- state.trace.record("run.warning", { message: `Failed to parse schema sidecar ${sidecar}: ${messageOf(e)}` });
1966
+ state.trace.record("run.warning", { message: `Failed to parse schema sidecar ${sidecar}: ${messageOf$1(e)}` });
1972
1967
  }
1973
1968
  if (!this.#deps.schemaInferer) return;
1974
1969
  for (const ext of ["ts", "js"]) {
@@ -1982,10 +1977,12 @@ var AgentLoop = class {
1982
1977
  async #handleScriptTool(call, skillName, scriptName, state) {
1983
1978
  const root = this.#deps.skillIndex.get(skillName);
1984
1979
  if (!root) return toolError("TOOL_NOT_FOUND", `Skill "${skillName}" is not in the catalog`);
1985
- let args = call.arguments;
1980
+ if (!this.#deps.executor) return toolError("TOOL_UNSUPPORTED", `No script executor is configured; script tool "${call.name}" cannot run`);
1986
1981
  const def = state.activatedTools.get(call.name);
1987
- const missing = (def?.inputSchema?.required ?? []).filter((key) => args[key] === void 0);
1988
- if (missing.length > 0 && this.#deps.uiBridge && this.#policy.missingParams === "user" && def?.inputSchema) try {
1982
+ if (!def) return toolError("TOOL_NOT_FOUND", `Tool "${call.name}" is not available for skill "${skillName}"`);
1983
+ let args = call.arguments;
1984
+ const missing = (def.inputSchema?.required ?? []).filter((key) => args[key] === void 0);
1985
+ if (missing.length > 0 && this.#deps.uiBridge && this.#policy.missingParams === "user" && def.inputSchema) try {
1989
1986
  const value = await this.#interact(state, {
1990
1987
  type: "form",
1991
1988
  id: this.#nextInteractionId(state),
@@ -2003,14 +2000,14 @@ var AgentLoop = class {
2003
2000
  if (e instanceof BridgeRequestError) return toolError("UI_UNAVAILABLE", `Parameter form failed: ${e.message}`);
2004
2001
  throw e;
2005
2002
  }
2006
- if (!this.#deps.executor) return toolError("TOOL_UNSUPPORTED", `No script executor is configured; script tool "${call.name}" cannot run`);
2007
2003
  const context = createScriptContext({
2008
2004
  fs: this.#deps.fs,
2009
2005
  artifactStore: this.#deps.artifactStore,
2010
2006
  skillName,
2011
2007
  skillRoot: root,
2012
2008
  runId: state.runId,
2013
- confirm: (message) => this.#confirm(message, state)
2009
+ confirm: (message) => this.#confirm(message, state),
2010
+ onWarning: (message) => state.trace.record("run.warning", { message })
2014
2011
  });
2015
2012
  try {
2016
2013
  const before = new Set((await this.#deps.artifactStore.listArtifacts(state.runId)).map((a) => a.id));
@@ -2028,7 +2025,7 @@ var AgentLoop = class {
2028
2025
  } catch (e) {
2029
2026
  if (e instanceof RunTerminated) throw e;
2030
2027
  await this.#bumpSkillStat(skillName, "failures", state);
2031
- return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `Tool "${call.name}" failed: ${messageOf(e)}`);
2028
+ return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `Tool "${call.name}" failed: ${messageOf$1(e)}`);
2032
2029
  }
2033
2030
  }
2034
2031
  /** context.confirm 触发点:默认真实询问;auto-approve 直通;无 bridge 降级直通 + warning */
@@ -2067,7 +2064,7 @@ var AgentLoop = class {
2067
2064
  try {
2068
2065
  await this.#deps.memory?.set(scope, key, value);
2069
2066
  } catch (e) {
2070
- state.trace.record("run.warning", { message: `Memory write failed: ${messageOf(e)}` });
2067
+ state.trace.record("run.warning", { message: `Memory write failed: ${messageOf$1(e)}` });
2071
2068
  }
2072
2069
  }
2073
2070
  async #writeActivationMemory(skillName, state) {
@@ -2323,6 +2320,393 @@ var WebSkillRuntime = class {
2323
2320
  }, this.#deps.config).resume(snapshot);
2324
2321
  }
2325
2322
  };
2323
+ /**
2324
+ * navigator.webskill 门面装配层:零新业务逻辑,全部直转
2325
+ * SkillDiscovery / validateSkills / WebSkillRuntime / SkillManager。
2326
+ */
2327
+ function createWebSkillApi(deps) {
2328
+ const { fs, roots } = deps;
2329
+ let runtime;
2330
+ const lazyRuntime = () => {
2331
+ runtime ??= new WebSkillRuntime({
2332
+ fs,
2333
+ roots,
2334
+ llm: deps.llm,
2335
+ ...deps.executor ? { executor: deps.executor } : {},
2336
+ ...deps.artifactStore ? { artifactStore: deps.artifactStore } : {},
2337
+ ...deps.loopConfig ? { config: deps.loopConfig } : {}
2338
+ });
2339
+ return runtime;
2340
+ };
2341
+ const requireManager = () => {
2342
+ if (!deps.skillManager) throw new WebSkillError("TOOL_UNSUPPORTED", "No skill manager is configured; install/uninstall is unavailable");
2343
+ return deps.skillManager;
2344
+ };
2345
+ return {
2346
+ async discover(path) {
2347
+ const { entries } = await new SkillDiscovery(fs, [path]).discover();
2348
+ return buildCatalog(entries);
2349
+ },
2350
+ async read(name) {
2351
+ const { entries } = await new SkillDiscovery(fs, roots).discover();
2352
+ const entry = entries.find((e) => e.name === name);
2353
+ if (!entry) throw new WebSkillError("SKILL_NOT_FOUND", `Unknown skill: ${name}`);
2354
+ const skillMdPath = `${entry.root}/SKILL.md`;
2355
+ const { metadata, body } = parseSkillMarkdown(await fs.readText(skillMdPath));
2356
+ return {
2357
+ metadata,
2358
+ body,
2359
+ location: {
2360
+ skillRoot: entry.root,
2361
+ skillMdPath,
2362
+ source: entry.source
2363
+ }
2364
+ };
2365
+ },
2366
+ validate(path) {
2367
+ return validateSkills(fs, [path]);
2368
+ },
2369
+ async run(prompt) {
2370
+ return (await lazyRuntime().run(prompt)).run;
2371
+ },
2372
+ async install(url) {
2373
+ const manifest = await requireManager().install(sourceFromUrl(url));
2374
+ return {
2375
+ name: manifest.name,
2376
+ ...manifest.version !== void 0 ? { version: manifest.version } : {},
2377
+ installedAt: manifest.installedAt,
2378
+ integrity: manifest.integrity
2379
+ };
2380
+ },
2381
+ async uninstall(name) {
2382
+ await requireManager().uninstall(name);
2383
+ }
2384
+ };
2385
+ }
2386
+ /** install URL 分派:http(s)…​.zip → http;…​.git → git(无 git 能力的 manager 自行 TOOL_UNSUPPORTED);其余 → local */
2387
+ function sourceFromUrl(url) {
2388
+ const bare = url.split("?")[0] ?? url;
2389
+ if (/^https?:\/\//.test(url) && bare.endsWith(".zip")) return {
2390
+ type: "http",
2391
+ url
2392
+ };
2393
+ if (bare.endsWith(".git")) return {
2394
+ type: "git",
2395
+ url
2396
+ };
2397
+ return {
2398
+ type: "local",
2399
+ path: url
2400
+ };
2401
+ }
2402
+ const messageOf = (e) => e instanceof Error ? e.message : String(e);
2403
+ /** 受控钩子执行器:逐个执行,超时/异常默认降级为 warning,可切严格模式 */
2404
+ var HookRunner = class {
2405
+ #hooks = /* @__PURE__ */ new Map();
2406
+ timeoutMs;
2407
+ failOnHookError;
2408
+ onWarning;
2409
+ constructor(options = {}) {
2410
+ this.timeoutMs = options.timeoutMs ?? 5e3;
2411
+ this.failOnHookError = options.failOnHookError ?? false;
2412
+ this.onWarning = options.onWarning;
2413
+ }
2414
+ register(phase, hook) {
2415
+ const key = phase;
2416
+ const list = this.#hooks.get(key) ?? [];
2417
+ list.push(hook);
2418
+ this.#hooks.set(key, list);
2419
+ return this;
2420
+ }
2421
+ async run(phase, ctx) {
2422
+ const hooks = [...this.#hooks.get(phase) ?? [], ...this.#hooks.get("*") ?? []];
2423
+ for (const hook of hooks) try {
2424
+ await this.#withTimeout(Promise.resolve().then(() => hook(ctx)), phase);
2425
+ } catch (e) {
2426
+ if (this.failOnHookError) throw e instanceof WebSkillError ? e : new WebSkillError("RUN_FAILED", messageOf(e), e);
2427
+ this.onWarning?.(messageOf(e));
2428
+ }
2429
+ }
2430
+ #withTimeout(promise, phase) {
2431
+ return new Promise((resolve, reject) => {
2432
+ const timer = setTimeout(() => reject(new WebSkillError("RUN_TIMEOUT", `Lifecycle hook for phase "${phase}" timed out after ${this.timeoutMs}ms`)), this.timeoutMs);
2433
+ promise.then((v) => {
2434
+ clearTimeout(timer);
2435
+ resolve(v);
2436
+ }, (e) => {
2437
+ clearTimeout(timer);
2438
+ reject(e instanceof Error ? e : new Error(String(e)));
2439
+ });
2440
+ });
2441
+ }
2442
+ };
2443
+ /** 内存 MemoryStore:测试与浏览器降级用 */
2444
+ var InMemoryStore = class {
2445
+ #data = /* @__PURE__ */ new Map();
2446
+ async get(scope, key) {
2447
+ return this.#data.get(scope)?.get(key);
2448
+ }
2449
+ async set(scope, key, value) {
2450
+ let bucket = this.#data.get(scope);
2451
+ if (!bucket) {
2452
+ bucket = /* @__PURE__ */ new Map();
2453
+ this.#data.set(scope, bucket);
2454
+ }
2455
+ bucket.set(key, value);
2456
+ }
2457
+ async delete(scope, key) {
2458
+ this.#data.get(scope)?.delete(key);
2459
+ }
2460
+ async list(scope) {
2461
+ return [...(this.#data.get(scope) ?? /* @__PURE__ */ new Map()).entries()].map(([key, value]) => ({
2462
+ key,
2463
+ value
2464
+ }));
2465
+ }
2466
+ async clear(scope) {
2467
+ if (scope === void 0) this.#data.clear();
2468
+ else this.#data.delete(scope);
2469
+ }
2470
+ };
2471
+ const encode = (s) => encodeURIComponent(s);
2472
+ /**
2473
+ * 基于 FileSystemProvider 的 MemoryStore:
2474
+ * <root>/<encodeURIComponent(scope)>/<encodeURIComponent(key)>.json
2475
+ * scope/key 全部编码防路径穿越;环境无关,构造必须显式传 fs。
2476
+ */
2477
+ var FsMemoryStore = class {
2478
+ #root;
2479
+ #fs;
2480
+ constructor(deps) {
2481
+ this.#root = deps.root.replace(/\/+$/, "");
2482
+ this.#fs = deps.fs;
2483
+ }
2484
+ #scopeDir(scope) {
2485
+ return `${this.#root}/${encode(scope)}`;
2486
+ }
2487
+ #keyPath(scope, key) {
2488
+ return `${this.#scopeDir(scope)}/${encode(key)}.json`;
2489
+ }
2490
+ async get(scope, key) {
2491
+ const path = this.#keyPath(scope, key);
2492
+ if (!await this.#fs.exists(path)) return void 0;
2493
+ return JSON.parse(await this.#fs.readText(path));
2494
+ }
2495
+ async set(scope, key, value) {
2496
+ await this.#fs.writeText(this.#keyPath(scope, key), JSON.stringify(value, null, 2));
2497
+ }
2498
+ async delete(scope, key) {
2499
+ const path = this.#keyPath(scope, key);
2500
+ if (await this.#fs.exists(path)) await this.#fs.remove(path);
2501
+ }
2502
+ async list(scope) {
2503
+ const dir = this.#scopeDir(scope);
2504
+ if (!await this.#fs.exists(dir)) return [];
2505
+ const out = [];
2506
+ for (const entry of await this.#fs.list(dir)) {
2507
+ if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
2508
+ const fileName = entry.path.split("/").pop() ?? "";
2509
+ const key = decodeURIComponent(fileName.replace(/\.json$/, ""));
2510
+ out.push({
2511
+ key,
2512
+ value: JSON.parse(await this.#fs.readText(entry.path))
2513
+ });
2514
+ }
2515
+ return out.sort((a, b) => a.key.localeCompare(b.key));
2516
+ }
2517
+ async clear(scope) {
2518
+ if (scope !== void 0) {
2519
+ const dir = this.#scopeDir(scope);
2520
+ if (await this.#fs.exists(dir)) await this.#fs.remove(dir, { recursive: true });
2521
+ return;
2522
+ }
2523
+ if (!await this.#fs.exists(this.#root)) return;
2524
+ for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
2525
+ }
2526
+ };
2527
+ const INDEX_FILE = "index.json";
2528
+ /**
2529
+ * 基于 FileSystemProvider 的 ArtifactStore:产物落盘 <root>/<runId>/<path>,
2530
+ * 每次写入同步更新 <root>/<runId>/index.json,新实例可凭索引恢复列表。
2531
+ * 环境无关(OPFS/Node 均可),构造必须显式传 fs。
2532
+ */
2533
+ var FsArtifactStore = class {
2534
+ #root;
2535
+ #fs;
2536
+ constructor(deps) {
2537
+ this.#root = deps.root.replace(/\/+$/, "");
2538
+ this.#fs = deps.fs;
2539
+ }
2540
+ async createTextArtifact(input) {
2541
+ const size = new TextEncoder().encode(input.content).length;
2542
+ await this.#fs.writeText(this.#artifactPath(input.runId, input.path), input.content);
2543
+ return this.#persist({
2544
+ ...input,
2545
+ type: "text",
2546
+ size
2547
+ });
2548
+ }
2549
+ async createBinaryArtifact(input) {
2550
+ await this.#fs.writeBinary(this.#artifactPath(input.runId, input.path), input.content);
2551
+ return this.#persist({
2552
+ ...input,
2553
+ type: "binary",
2554
+ size: input.content.length
2555
+ });
2556
+ }
2557
+ async listArtifacts(runId) {
2558
+ const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
2559
+ if (!await this.#fs.exists(indexPath)) return [];
2560
+ const raw = await this.#fs.readText(indexPath);
2561
+ return JSON.parse(raw).artifacts ?? [];
2562
+ }
2563
+ #artifactPath(runId, artifactPath) {
2564
+ return resolveInsideRoot(`${this.#root}/${runId}`, artifactPath);
2565
+ }
2566
+ async #persist(input) {
2567
+ const artifact = {
2568
+ id: `${input.runId}/${input.path}`,
2569
+ runId: input.runId,
2570
+ path: input.path,
2571
+ type: input.type,
2572
+ mimeType: input.mimeType,
2573
+ size: input.size,
2574
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2575
+ metadata: input.metadata
2576
+ };
2577
+ const next = [...(await this.listArtifacts(input.runId)).filter((a) => a.id !== artifact.id), artifact];
2578
+ await this.#fs.writeText(`${this.#root}/${input.runId}/${INDEX_FILE}`, JSON.stringify({ artifacts: next }, null, 2));
2579
+ return artifact;
2580
+ }
2581
+ };
2582
+ const isRecord = (v) => typeof v === "object" && v !== null;
2583
+ const isNonEmptyString = (v) => typeof v === "string" && v !== "";
2584
+ /** 入站校验:非法 kind / 缺 id / 缺字段 → undefined(调用方忽略并记 warning) */
2585
+ function parseBridgeRequest(data) {
2586
+ if (!isRecord(data)) return void 0;
2587
+ const { kind, id } = data;
2588
+ if (!isNonEmptyString(id)) return void 0;
2589
+ switch (kind) {
2590
+ case "readReference": return isNonEmptyString(data["path"]) ? {
2591
+ kind,
2592
+ id,
2593
+ path: data["path"]
2594
+ } : void 0;
2595
+ case "writeArtifact": {
2596
+ const { path, content, mimeType } = data;
2597
+ const validContent = typeof content === "string" || Array.isArray(content) && content.every((n) => typeof n === "number");
2598
+ if (!isNonEmptyString(path) || !validContent) return void 0;
2599
+ return {
2600
+ kind,
2601
+ id,
2602
+ path,
2603
+ content,
2604
+ ...isNonEmptyString(mimeType) ? { mimeType } : {}
2605
+ };
2606
+ }
2607
+ case "confirm": return isNonEmptyString(data["message"]) ? {
2608
+ kind,
2609
+ id,
2610
+ message: data["message"]
2611
+ } : void 0;
2612
+ default: return;
2613
+ }
2614
+ }
2615
+ function bridgeError(id, code, message) {
2616
+ return {
2617
+ id,
2618
+ ok: false,
2619
+ error: {
2620
+ code,
2621
+ message
2622
+ }
2623
+ };
2624
+ }
2625
+ /**
2626
+ * 判定 URL 是否被策略放行:
2627
+ * - 'deny-all'(默认)全拒;'allow-all' 全放
2628
+ * - { allow } 条目支持三种写法:
2629
+ * - 精确域名 'api.example.com'(忽略端口与路径)
2630
+ * - 通配 '*.example.com'(含 apex 与任意深度子域)
2631
+ * - 源 'http://localhost:3000'(协议 + host + port 全等)
2632
+ * - URL 解析失败一律拒绝
2633
+ */
2634
+ function isNetworkAllowed(policy, url) {
2635
+ if (policy === "allow-all") return true;
2636
+ if (policy === "deny-all" || !policy || typeof policy !== "object") return false;
2637
+ var allow = policy.allow;
2638
+ if (!Array.isArray(allow)) return false;
2639
+ var parsed;
2640
+ try {
2641
+ parsed = new URL(url);
2642
+ } catch {
2643
+ return false;
2644
+ }
2645
+ var host = parsed.hostname.toLowerCase();
2646
+ for (var i = 0; i < allow.length; i++) {
2647
+ var entry = allow[i];
2648
+ if (typeof entry !== "string" || entry === "") continue;
2649
+ if (entry.indexOf("://") !== -1) {
2650
+ try {
2651
+ if (new URL(entry).origin === parsed.origin) return true;
2652
+ } catch {}
2653
+ continue;
2654
+ }
2655
+ var rule = entry.toLowerCase();
2656
+ if (rule.indexOf("*.") === 0) {
2657
+ var suffix = rule.slice(2);
2658
+ if (host === suffix || host.endsWith("." + suffix)) return true;
2659
+ } else if (host === rule) return true;
2660
+ }
2661
+ return false;
2662
+ }
2663
+ /** 阻断 trace 用的脱敏 host(解析失败返回占位,不记录完整 URL) */
2664
+ function networkUrlHost(url) {
2665
+ try {
2666
+ return new URL(url).hostname;
2667
+ } catch {
2668
+ return "(unparseable-url)";
2669
+ }
2670
+ }
2671
+ /**
2672
+ * Per-capability 强制授权判定(宿主侧,browser/node 执行器共用单一来源)。
2673
+ * 'require-approval' 模式下经注入的 UiBridge 发 authorize 交互;
2674
+ * once-per-run(默认)在 run 内记忆已批准能力,every-call 每次调用都问。
2675
+ */
2676
+ var CapabilityApproval = class {
2677
+ #uiBridge;
2678
+ #scope;
2679
+ /** runId → 已批准能力集合(once-per-run 记忆) */
2680
+ #approved = /* @__PURE__ */ new Map();
2681
+ #seq = 0;
2682
+ constructor(deps = {}) {
2683
+ this.#uiBridge = deps.uiBridge;
2684
+ this.#scope = deps.scope ?? "once-per-run";
2685
+ }
2686
+ async authorize(input) {
2687
+ const { runId, capability, mode } = input;
2688
+ if (mode === false) return "disabled";
2689
+ if (mode !== "require-approval") return "allowed";
2690
+ if (this.#scope === "once-per-run" && this.#approved.get(runId)?.has(capability)) return "allowed";
2691
+ if (!this.#uiBridge) return "denied";
2692
+ if ((await this.#uiBridge.request({
2693
+ type: "authorize",
2694
+ id: `authorize-${runId}-${capability}-${++this.#seq}`,
2695
+ capability,
2696
+ message: input.message,
2697
+ ...input.details !== void 0 ? { details: input.details } : {}
2698
+ })).value !== true) return "denied";
2699
+ if (this.#scope === "once-per-run") {
2700
+ let set = this.#approved.get(runId);
2701
+ if (!set) {
2702
+ set = /* @__PURE__ */ new Set();
2703
+ this.#approved.set(runId, set);
2704
+ }
2705
+ set.add(capability);
2706
+ }
2707
+ return "allowed";
2708
+ }
2709
+ };
2326
2710
 
2327
2711
  //#endregion
2328
- export { renderCatalogJson as $, parseBridgeRequest as A, SkillDiscovery as B, bridgeError as C, fromVercelStreamPart as D, fromVercelResult as E, MemoryFS as F, checkSkillRules as G, WebSkillError as H, SKILLS_LOCKFILE as I, isValidSkillName as J, computeDigest as K, SKILL_MANIFEST_FILE as L, schemaToForm as M, toLlmToolSpec as N, mergeCatalogEntries as O, toVercelToolSpecs as P, renderAvailableSkillsXml as Q, SKILL_NAME_MAX_LENGTH as R, WebSkillRuntime as S, createScriptContext as T, buildCatalog as U, SkillReader as V, buildManifest as W, normalizePath as X, jsonRenderer as Y, parseSkillMarkdown as Z, READ_SKILL_FILE_INPUT_SCHEMA as _, EventBus as a, RUN_SNAPSHOT_SCHEMA_VERSION as b, FsRunSnapshotStore as c, InMemoryStore as d, resolveInsideRoot as et, MemoryArtifactStore as f, ProgressiveRouter as g, OpenAiCompatibleClient as h, AgentLoop as i, resolveToolName as j, normalizeToolContent as k, FullDisclosureRouter as l, MockUiBridge as m, ASK_USER_TOOL as n, verifyManifest as nt, FsArtifactStore as o, MockLlmClient as p, escapeXml as q, ASK_USER_TOOL_NAME as r, xmlRenderer as rt, FsMemoryStore as s, ASK_USER_INPUT_SCHEMA as t, validateSkills as tt, HookRunner as u, READ_SKILL_FILE_TOOL as v, buildRenderResult as w, TraceRecorder as x, READ_SKILL_FILE_TOOL_NAME as y, SKILL_NAME_PATTERN as z };
2712
+ export { computeDigest as $, fromVercelStreamPart as A, MemoryFS as B, WebSkillRuntime as C, createWebSkillApi as D, createScriptContext as E, parseBridgeRequest as F, SKILL_PACK_FILE as G, SKILL_MANIFEST_FILE as H, resolveToolName as I, WebSkillError as J, SkillDiscovery as K, schemaToForm as L, mergeCatalogEntries as M, networkUrlHost as N, extractChartSpec as O, normalizeToolContent as P, checkSkillRules as Q, toLlmToolSpec as R, TraceRecorder as S, buildRenderResult as T, SKILL_NAME_MAX_LENGTH as U, SKILLS_LOCKFILE as V, SKILL_NAME_PATTERN as W, buildManifest as X, buildCatalog as Y, checkDependencyCycles as Z, ProgressiveRouter as _, CapabilityApproval as a, parseSkillMarkdown as at, READ_SKILL_FILE_TOOL_NAME as b, FsMemoryStore as c, renderCatalogJson as ct, HookRunner as d, verifyManifest as dt, escapeXml as et, InMemoryStore as f, xmlRenderer as ft, OpenAiCompatibleClient as g, MockUiBridge as h, AgentLoop as i, normalizePath as it, isNetworkAllowed as j, fromVercelResult as k, FsRunSnapshotStore as l, resolveInsideRoot as lt, MockLlmClient as m, ASK_USER_TOOL as n, isValidSkillName as nt, EventBus as o, parseSkillPackManifest as ot, MemoryArtifactStore as p, SkillReader as q, ASK_USER_TOOL_NAME as r, jsonRenderer as rt, FsArtifactStore as s, renderAvailableSkillsXml as st, ASK_USER_INPUT_SCHEMA as t, exportSkills as tt, FullDisclosureRouter as u, validateSkills as ut, READ_SKILL_FILE_INPUT_SCHEMA as v, bridgeError as w, RUN_SNAPSHOT_SCHEMA_VERSION as x, READ_SKILL_FILE_TOOL as y, toVercelToolSpecs as z };