@clack-platform/cli 0.1.1 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +24 -3
  2. package/dist/cli.js +1178 -131
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -150,7 +150,7 @@ function redact(value2) {
150
150
  // package.json
151
151
  var package_default = {
152
152
  name: "@clack-platform/cli",
153
- version: "0.1.1",
153
+ version: "0.2.0",
154
154
  description: "\uD074\uB799 \uACC4\uC815\uC758 \uC0C1\uD488\xB7\uCF58\uD150\uCE20\xB7\uD504\uB85C\uD544\uC744 \uAD00\uB9AC\uD558\uB294 CLI",
155
155
  type: "module",
156
156
  bin: {
@@ -399,9 +399,9 @@ function documentPath(collection2, key) {
399
399
  return `/platform/v1/data/${collection2}/docs/${key}`;
400
400
  }
401
401
  function bodySize(body) {
402
- const bytes = Buffer.byteLength(JSON.stringify(body), "utf8");
403
- if (bytes > maxBodyBytes) throw new CliError("\uBB38\uC11C \uBCF8\uBB38\uC740 64 KiB \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4.", "VALIDATION_ERROR");
404
- return bytes;
402
+ const bytes2 = Buffer.byteLength(JSON.stringify(body), "utf8");
403
+ if (bytes2 > maxBodyBytes) throw new CliError("\uBB38\uC11C \uBCF8\uBB38\uC740 64 KiB \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4.", "VALIDATION_ERROR");
404
+ return bytes2;
405
405
  }
406
406
  async function readPlatformBodyFile(file) {
407
407
  let raw;
@@ -598,11 +598,11 @@ async function authoringRequest(client, raw, surface = "cli") {
598
598
  if (input.action === "upload") {
599
599
  const stat3 = await lstat2(input.file);
600
600
  if (!stat3.isFile() || stat3.isSymbolicLink() || stat3.size > 4 * 1024 * 1024 || !stat3.size) throw new CliError("\uC774\uBBF8\uC9C0\uB294 \uC77C\uBC18 \uD30C\uC77C\uC774\uBA70 4 MiB \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
601
- const bytes = await readFile2(input.file);
601
+ const bytes2 = await readFile2(input.file);
602
602
  const body2 = new FormData();
603
603
  body2.append("field", input.field);
604
604
  if (input.item_index !== void 0) body2.append("item_index", String(input.item_index));
605
- body2.append("file", new Blob([bytes]), basename(input.file));
605
+ body2.append("file", new Blob([bytes2]), basename(input.file));
606
606
  return client.request("POST", `${path}/assets`, { body: body2 });
607
607
  }
608
608
  if (input.action === "image" || input.action === "image-batch") {
@@ -1262,9 +1262,9 @@ function isRemoteImage(value2) {
1262
1262
  return false;
1263
1263
  }
1264
1264
  }
1265
- function dimensions(bytes) {
1265
+ function dimensions(bytes2) {
1266
1266
  try {
1267
- const result = imageSize(bytes);
1267
+ const result = imageSize(bytes2);
1268
1268
  if (!result.width || !result.height || result.width > 2e4 || result.height > 2e4 || !result.type || !MIME[result.type]) throw new Error();
1269
1269
  return { width: result.width, height: result.height, mime: MIME[result.type] };
1270
1270
  } catch {
@@ -1291,26 +1291,26 @@ async function prepareImage(source, baseDir = process.cwd(), measureRemote = fal
1291
1291
  } finally {
1292
1292
  await reader.cancel();
1293
1293
  }
1294
- const bytes2 = new Uint8Array(size);
1294
+ const bytes3 = new Uint8Array(size);
1295
1295
  let offset = 0;
1296
1296
  for (const chunk of chunks) {
1297
- bytes2.set(chunk, offset);
1297
+ bytes3.set(chunk, offset);
1298
1298
  offset += chunk.length;
1299
1299
  }
1300
- return { source, ...dimensions(bytes2) };
1300
+ return { source, ...dimensions(bytes3) };
1301
1301
  }
1302
1302
  if (/^[a-z][a-z0-9+.-]*:/i.test(source) && !/^[a-z]:[\\/]/i.test(source)) throw new CliError("\uC774\uBBF8\uC9C0\uB294 \uB85C\uCEEC \uD30C\uC77C \uB610\uB294 http(s) URL\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.");
1303
1303
  const path = resolve(baseDir, source);
1304
- let bytes;
1304
+ let bytes2;
1305
1305
  try {
1306
1306
  const info = await stat(path);
1307
1307
  if (!info.isFile() || info.size === 0 || info.size > MAX_IMAGE_BYTES) throw new Error();
1308
- bytes = await readFile5(path);
1309
- if (bytes.length > MAX_IMAGE_BYTES) throw new Error();
1308
+ bytes2 = await readFile5(path);
1309
+ if (bytes2.length > MAX_IMAGE_BYTES) throw new Error();
1310
1310
  } catch {
1311
1311
  throw new CliError(`10MB \uC774\uD558 \uC774\uBBF8\uC9C0 \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${path}`);
1312
1312
  }
1313
- return { source: path, bytes, ...dimensions(bytes) };
1313
+ return { source: path, bytes: bytes2, ...dimensions(bytes2) };
1314
1314
  }
1315
1315
  async function uploadPrepared(ctx, image2) {
1316
1316
  if (!image2.bytes) return { originalUrl: image2.source };
@@ -1998,6 +1998,11 @@ import { createInterface } from "readline";
1998
1998
  import { z as z10 } from "zod";
1999
1999
 
2000
2000
  // src/commands/skill.ts
2001
+ import { spawn } from "child_process";
2002
+ import { existsSync } from "fs";
2003
+ import { writeFile as writeFile2 } from "fs/promises";
2004
+ import { createRequire } from "module";
2005
+ import { join as join3, resolve as resolve8 } from "path";
2001
2006
  import { z as z9 } from "zod";
2002
2007
 
2003
2008
  // src/commands/content.ts
@@ -2034,15 +2039,15 @@ var CAPABILITIES = /* @__PURE__ */ new Set([
2034
2039
  function isObject(value2) {
2035
2040
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
2036
2041
  }
2037
- function parseObject(bytes) {
2042
+ function parseObject(bytes2) {
2038
2043
  try {
2039
- const value2 = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
2044
+ const value2 = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes2));
2040
2045
  return isObject(value2) ? value2 : null;
2041
2046
  } catch {
2042
2047
  return null;
2043
2048
  }
2044
2049
  }
2045
- function inspectContentPackage(bytes) {
2050
+ function inspectContentPackage(bytes2) {
2046
2051
  const warnings = [];
2047
2052
  const paths = /* @__PURE__ */ new Set();
2048
2053
  const profileFiles = /* @__PURE__ */ new Set();
@@ -2061,7 +2066,7 @@ function inspectContentPackage(bytes) {
2061
2066
  let tooLargeExpanded = false;
2062
2067
  let files;
2063
2068
  try {
2064
- files = unzipSync(bytes, { filter(entry) {
2069
+ files = unzipSync(bytes2, { filter(entry) {
2065
2070
  entries++;
2066
2071
  if (entries > MAX_FILES) {
2067
2072
  tooManyFiles = true;
@@ -2148,27 +2153,27 @@ async function prepareContentFile(source) {
2148
2153
  const file = resolve6(source);
2149
2154
  const extension = extname(file).toLowerCase();
2150
2155
  if (![".html", ".htm", ".zip"].includes(extension)) throw new CliError("HTML \uB610\uB294 ZIP \uD30C\uC77C\uC744 \uC9C0\uC815\uD558\uC138\uC694.");
2151
- let bytes;
2156
+ let bytes2;
2152
2157
  try {
2153
2158
  const info = await stat2(file);
2154
2159
  if (!info.isFile() || info.size === 0 || info.size > MAX_BYTES) throw new Error();
2155
- bytes = await readFile8(file);
2156
- if (!bytes.length || bytes.length > MAX_BYTES) throw new Error();
2160
+ bytes2 = await readFile8(file);
2161
+ if (!bytes2.length || bytes2.length > MAX_BYTES) throw new Error();
2157
2162
  } catch {
2158
2163
  throw new CliError("30 MiB \uC774\uD558\uC758 \uBE44\uC5B4 \uC788\uC9C0 \uC54A\uC740 \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC788\uC5B4\uC57C \uD569\uB2C8\uB2E4.");
2159
2164
  }
2160
2165
  return {
2161
- bytes,
2162
- warnings: extension === ".zip" ? inspectContentPackage(bytes) : [],
2166
+ bytes: bytes2,
2167
+ warnings: extension === ".zip" ? inspectContentPackage(bytes2) : [],
2163
2168
  metadata: {
2164
2169
  filename: basename3(file),
2165
2170
  content_type: extension === ".zip" ? "application/zip" : "text/html",
2166
- byte_size: bytes.length,
2167
- sha256: createHash2("sha256").update(bytes).digest("hex")
2171
+ byte_size: bytes2.length,
2172
+ sha256: createHash2("sha256").update(bytes2).digest("hex")
2168
2173
  }
2169
2174
  };
2170
2175
  }
2171
- async function putContentFile(session2, bytes, fetcher = fetch) {
2176
+ async function putContentFile(session2, bytes2, fetcher = fetch) {
2172
2177
  uuid(session2.upload_id);
2173
2178
  let url;
2174
2179
  try {
@@ -2179,7 +2184,7 @@ async function putContentFile(session2, bytes, fetcher = fetch) {
2179
2184
  }
2180
2185
  const contentType = session2.headers?.["Content-Type"];
2181
2186
  const checksum = session2.headers?.["x-amz-checksum-sha256"];
2182
- if (!["text/html", "application/zip"].includes(contentType ?? "") || checksum !== createHash2("sha256").update(bytes).digest("base64")) {
2187
+ if (!["text/html", "application/zip"].includes(contentType ?? "") || checksum !== createHash2("sha256").update(bytes2).digest("base64")) {
2183
2188
  throw new CliError("\uC5C5\uB85C\uB4DC \uCCB4\uD06C\uC12C\uACFC \uD30C\uC77C \uD615\uC2DD\uC774 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.", "INVALID_UPLOAD_RESPONSE", 502);
2184
2189
  }
2185
2190
  rememberSecret(session2.upload_url);
@@ -2187,7 +2192,7 @@ async function putContentFile(session2, bytes, fetcher = fetch) {
2187
2192
  const result = await fetcher(url, {
2188
2193
  method: "PUT",
2189
2194
  headers: { "Content-Type": contentType, "x-amz-checksum-sha256": checksum },
2190
- body: new Uint8Array(bytes),
2195
+ body: new Uint8Array(bytes2),
2191
2196
  redirect: "error",
2192
2197
  signal: AbortSignal.timeout(12e4)
2193
2198
  });
@@ -2213,7 +2218,7 @@ function registerContentCommands(program2, runtime2) {
2213
2218
  const body = parse(createSchema, { title: opts.title, description: opts.description, kind: opts.kind, policy_version: opts.policyVersion });
2214
2219
  await mutate(ctx, "POST", "/v4/creator/contents", body);
2215
2220
  });
2216
- runtime2.action(content.command("upload <id> <file>").description("HTML/ZIP \uC5C5\uB85C\uB4DC\uB85C \uC0C8 \uBE44\uACF5\uAC1C \uBC84\uC804 \uC0DD\uC131 (creator-content:write)").addOption(new Option("--header <mode>", "\uD5E4\uB354 \uD45C\uC2DC").choices(["fixed", "scroll_hide", "translucent_scroll_hide", "floating_close"])).addOption(new Option("--color <scheme>", "\uD5E4\uB354 \uC0C9\uC0C1").choices(["light", "dark"])), async (ctx, [id2, file], opts) => {
2221
+ runtime2.action(content.command("upload <id> <file>").description("HTML/ZIP \uC5C5\uB85C\uB4DC\uB85C \uC0C8 \uBE44\uACF5\uAC1C \uBC84\uC804 \uC0DD\uC131 (creator-content:write)").addOption(new Option("--header <mode>", "\uD5E4\uB354 \uD45C\uC2DC").choices(["fixed", "scroll_hide", "translucent_scroll_hide", "floating_close", "floating_close_safe_area"])).addOption(new Option("--color <scheme>", "\uD5E4\uB354 \uC0C9\uC0C1").choices(["light", "dark"])), async (ctx, [id2, file], opts) => {
2217
2222
  const path = `/v4/creator/contents/${uuid(id2)}`;
2218
2223
  const prepared = await prepareContentFile(file);
2219
2224
  for (const warning of prepared.warnings) process.stderr.write(`\uD328\uD0A4\uC9C0 \uACBD\uACE0: ${warning}
@@ -2271,36 +2276,155 @@ function registerContentCommands(program2, runtime2) {
2271
2276
  }
2272
2277
 
2273
2278
  // src/vendor/clack-api-v4-contracts/authoring-semantics.mjs
2274
- function inspectAuthoringSemantics(skill, form) {
2279
+ function walkDataSchema(schema, visit) {
2280
+ const defs = schema?.$defs ?? {};
2281
+ const step = (node, info) => {
2282
+ if (!node || typeof node !== "object" || Array.isArray(node)) return;
2283
+ if (typeof node.$ref === "string") {
2284
+ const key = node.$ref.startsWith("#/$defs/") ? node.$ref.slice("#/$defs/".length) : null;
2285
+ if (key === null || !Object.hasOwn(defs, key) || info.refs.includes(key)) {
2286
+ visit(node, { ...info, cycle: key !== null && info.refs.includes(key), unresolved: key === null || !Object.hasOwn(defs, key) });
2287
+ return;
2288
+ }
2289
+ visit(node, info);
2290
+ step(defs[key], { ...info, refs: [...info.refs, key] });
2291
+ return;
2292
+ }
2293
+ visit(node, info);
2294
+ for (const branch of Array.isArray(node.oneOf) ? node.oneOf : []) step(branch, { ...info, branch: true });
2295
+ for (const [name2, child] of Object.entries(node.properties && typeof node.properties === "object" ? node.properties : {})) {
2296
+ step(child, {
2297
+ ...info,
2298
+ pointer: `${info.pointer}/${name2}`,
2299
+ depth: info.depth + 1,
2300
+ root: info.depth === 0 ? name2 : info.root,
2301
+ visibility: info.depth === 0 ? child?.["x-clack-visibility"] : info.visibility,
2302
+ branch: false
2303
+ });
2304
+ }
2305
+ if (node.items && typeof node.items === "object") step(node.items, { ...info, pointer: `${info.pointer}/*`, depth: info.depth + 1, branch: false });
2306
+ };
2307
+ step(schema, { pointer: "", depth: 0, root: null, visibility: null, refs: [], branch: false });
2308
+ }
2309
+ function dataRootField(schema, name2) {
2310
+ const field2 = schema?.properties?.[name2];
2311
+ if (!field2 || typeof field2 !== "object") return null;
2312
+ if (typeof field2.$ref === "string" && field2.$ref.startsWith("#/$defs/")) {
2313
+ const target = schema.$defs?.[field2.$ref.slice("#/$defs/".length)];
2314
+ return target && typeof target === "object" ? { ...target, "x-clack-visibility": field2["x-clack-visibility"] } : null;
2315
+ }
2316
+ return field2;
2317
+ }
2318
+ function inspectEditorSemantics(skill, data, fail2) {
2319
+ const editor = skill.authoring?.editor;
2320
+ if (!editor) return;
2321
+ const permissions = new Set(editor.permissions ?? []);
2322
+ if (!permissions.has("document.read") || !permissions.has("document.write")) fail2("/authoring/editor/permissions", "\uC5D0\uB514\uD130\uC5D0\uB294 document.read\uC640 document.write \uAD8C\uD55C\uC774 \uBAA8\uB450 \uD544\uC694\uD569\uB2C8\uB2E4.");
2323
+ if (permissions.has("ai.suggest") && skill.authoring?.ai_fill?.enabled !== true) fail2("/authoring/editor/permissions", "ai.suggest \uAD8C\uD55C\uC5D0\uB294 authoring.ai_fill.enabled\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
2324
+ const imageTool = (skill.authoring?.tools ?? []).find((tool) => tool.tool === "image.generate");
2325
+ if (permissions.has("tools.image.generate") && !imageTool) fail2("/authoring/editor/permissions", "tools.image.generate \uAD8C\uD55C\uC5D0\uB294 authoring.tools\uC758 image.generate \uC120\uC5B8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
2326
+ if (imageTool?.aspect === "slot") fail2("/authoring/tools", "\uC5D0\uB514\uD130 \uC2A4\uD0AC\uC740 \uC2AC\uB86F \uBE44\uC728(aspect: slot)\uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC790\uC0B0 \uB178\uB4DC\uC758 x-clack-asset.aspect\uB97C \uC4F0\uC138\uC694.");
2327
+ if (!data) return;
2328
+ let assets = 0;
2329
+ let suggest = 0;
2330
+ walkDataSchema(data, (node) => {
2331
+ if (node["x-clack-asset"]) assets++;
2332
+ if (node["x-clack-ai"]?.suggest === true) suggest++;
2333
+ });
2334
+ if (assets > 0 && !skill.output?.data?.assets) fail2("/output/data/assets", "\uC790\uC0B0 \uB178\uB4DC\uAC00 \uC788\uB294 \uB370\uC774\uD130 \uC2A4\uD0A4\uB9C8\uC5D0\uB294 output.data.assets \uD55C\uB3C4\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
2335
+ if (assets === 0 && skill.output?.data?.assets) fail2("/output/data/assets", "\uC790\uC0B0 \uB178\uB4DC\uAC00 \uC5C6\uB294\uB370 \uC790\uC0B0 \uD55C\uB3C4\uB97C \uC120\uC5B8\uD588\uC2B5\uB2C8\uB2E4.");
2336
+ if (assets === 0 && permissions.has("tools.image.generate")) fail2("/authoring/editor/permissions", "\uC774\uBBF8\uC9C0\uB97C \uB123\uC744 \uC790\uC0B0 \uB178\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
2337
+ if (suggest === 0 && permissions.has("ai.suggest")) fail2("/authoring/editor/permissions", "x-clack-ai.suggest\uAC00 \uCF1C\uC9C4 \uB178\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
2338
+ const metadata = skill.output?.metadata ?? {};
2339
+ for (const key of ["title", "description"]) {
2340
+ if (metadata[key] === void 0) continue;
2341
+ const field2 = dataRootField(data, metadata[key]);
2342
+ if (!field2 || field2.type !== "string" || field2["x-clack-asset"] || field2["x-clack-visibility"] !== "public") fail2(`/output/metadata/${key}`, "\uACF5\uAC1C \uBB38\uC790\uC5F4 \uB8E8\uD2B8 \uD0A4\uB97C \uAC00\uB9AC\uCF1C\uC57C \uD569\uB2C8\uB2E4.");
2343
+ }
2344
+ for (const key of ["thumbnail", "thumbnail_fallback"]) {
2345
+ if (metadata[key] === void 0) continue;
2346
+ const field2 = dataRootField(data, metadata[key]);
2347
+ if (!field2 || !field2["x-clack-asset"] || field2["x-clack-visibility"] !== "public") fail2(`/output/metadata/${key}`, "\uACF5\uAC1C \uC790\uC0B0 \uB8E8\uD2B8 \uD0A4\uB97C \uAC00\uB9AC\uCF1C\uC57C \uD569\uB2C8\uB2E4(\uC778\uB371\uC2A4 \uD45C\uAE30 \uBD88\uAC00).");
2348
+ }
2349
+ }
2350
+ var INTRO_MAX_IMAGES = 8;
2351
+ var INTRO_CREATOR_COMMENT_MAX = 500;
2352
+ var INTRO_DESCRIPTION_MAX = 5e3;
2353
+ var INTRO_TEXT_WIDGETS = /* @__PURE__ */ new Set(["text", "textarea"]);
2354
+ function introFieldLabel(name2, field2) {
2355
+ const title = typeof field2?.title === "string" ? field2.title.normalize("NFC").trim() : "";
2356
+ return title || name2;
2357
+ }
2358
+ function inspectIntroSemantics(skill, form, slots, fail2) {
2359
+ const intro = skill.output?.intro;
2360
+ if (!intro) return;
2361
+ let images = 0;
2362
+ for (const name2 of intro.images ?? []) {
2363
+ const slot = slots.find((entry) => entry.field === name2);
2364
+ if (!slot) {
2365
+ fail2("/output/intro/images", "\uC778\uD2B8\uB85C \uC774\uBBF8\uC9C0\uB294 output.asset_slots\uC758 \uD544\uB4DC\uB9CC \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
2366
+ continue;
2367
+ }
2368
+ images += slot.multiple ? slot.multiple.max : 1;
2369
+ }
2370
+ if (images > INTRO_MAX_IMAGES) fail2("/output/intro/images", `\uC778\uD2B8\uB85C \uC774\uBBF8\uC9C0\uB294 \uC2AC\uB86F \uCD5C\uB300 \uC7A5\uC218 \uD569\uACC4\uAC00 ${INTRO_MAX_IMAGES}\uC7A5 \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
2371
+ if (!form) return;
2372
+ const properties = form.properties ?? {};
2373
+ const textField = (name2) => {
2374
+ const field2 = Object.hasOwn(properties, name2) ? properties[name2] : null;
2375
+ return field2 && field2.type === "string" && INTRO_TEXT_WIDGETS.has(field2["x-clack-widget"]) && Number.isSafeInteger(field2.maxLength) ? field2 : null;
2376
+ };
2377
+ for (const [key, max] of [["creator_comment", INTRO_CREATOR_COMMENT_MAX], ["description", INTRO_DESCRIPTION_MAX]]) {
2378
+ if (intro[key] === void 0) continue;
2379
+ const field2 = textField(intro[key]);
2380
+ if (!field2 || field2["x-clack-visibility"] !== "public") fail2(`/output/intro/${key}`, "\uACF5\uAC1C \uBB38\uC790\uC5F4(text\xB7textarea) \uD3FC \uD544\uB4DC\uB97C \uAC00\uB9AC\uCF1C\uC57C \uD569\uB2C8\uB2E4.");
2381
+ else if (field2.maxLength > max) fail2(`/output/intro/${key}`, `\uAC00\uB9AC\uD0A4\uB294 \uD3FC \uD544\uB4DC\uC758 \uCD5C\uB300 \uAE38\uC774\uAC00 ${max}\uC790 \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
2382
+ }
2383
+ const fallback = intro.description_fallback ?? [];
2384
+ let composed = 0;
2385
+ for (const name2 of fallback) {
2386
+ const field2 = textField(name2);
2387
+ if (!field2) {
2388
+ fail2("/output/intro/description_fallback", "\uBB38\uC790\uC5F4(text\xB7textarea) \uD3FC \uD544\uB4DC\uB9CC \uC790\uB3D9 \uAD6C\uC131\uC5D0 \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
2389
+ continue;
2390
+ }
2391
+ composed += introFieldLabel(name2, field2).length + 1 + field2.maxLength;
2392
+ }
2393
+ composed += Math.max(0, fallback.length - 1) * 2;
2394
+ if (composed > INTRO_DESCRIPTION_MAX) fail2("/output/intro/description_fallback", `\uC790\uB3D9 \uAD6C\uC131 \uCD5C\uB300 \uAE38\uC774(\uB77C\uBCA8 \uD3EC\uD568)\uAC00 ${INTRO_DESCRIPTION_MAX}\uC790\uB97C \uB118\uC2B5\uB2C8\uB2E4.`);
2395
+ }
2396
+ function inspectAuthoringSemantics(skill, form, data) {
2275
2397
  const errors = [];
2276
- const fail = (path, message) => errors.push({ path, message });
2398
+ const fail2 = (path, message) => errors.push({ path, message });
2399
+ inspectEditorSemantics(skill, data, fail2);
2277
2400
  const tools = skill.authoring?.tools ?? [];
2278
- if (new Set(tools.map((tool) => tool.tool)).size !== tools.length) fail("/authoring/tools", "\uB3C4\uAD6C \uC774\uB984\uC744 \uC911\uBCF5 \uC120\uC5B8\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2279
- for (const tool of tools) if (tool.batch_max > tool.max_calls) fail("/authoring/tools", "\uC77C\uAD04 \uC0DD\uC131 \uC218\uB294 \uC138\uC158 \uD638\uCD9C \uC218\uB97C \uB118\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2401
+ if (new Set(tools.map((tool) => tool.tool)).size !== tools.length) fail2("/authoring/tools", "\uB3C4\uAD6C \uC774\uB984\uC744 \uC911\uBCF5 \uC120\uC5B8\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2402
+ for (const tool of tools) if (tool.batch_max > tool.max_calls) fail2("/authoring/tools", "\uC77C\uAD04 \uC0DD\uC131 \uC218\uB294 \uC138\uC158 \uD638\uCD9C \uC218\uB97C \uB118\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2280
2403
  const slots = skill.output?.asset_slots ?? [];
2281
2404
  const paths = /* @__PURE__ */ new Set();
2282
2405
  const fields3 = /* @__PURE__ */ new Set();
2283
2406
  for (const slot of slots) {
2284
- if (fields3.has(slot.field)) fail("/output/asset_slots", "\uC2AC\uB86F \uD544\uB4DC\uB294 \uC911\uBCF5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2407
+ if (fields3.has(slot.field)) fail2("/output/asset_slots", "\uC2AC\uB86F \uD544\uB4DC\uB294 \uC911\uBCF5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2285
2408
  fields3.add(slot.field);
2286
- if (slot.multiple && slot.multiple.min > slot.multiple.max) fail("/output/asset_slots", "\uB2E4\uC911 \uC2AC\uB86F \uCD5C\uC18C \uC218\uB294 \uCD5C\uB300 \uC218\uBCF4\uB2E4 \uD074 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2409
+ if (slot.multiple && slot.multiple.min > slot.multiple.max) fail2("/output/asset_slots", "\uB2E4\uC911 \uC2AC\uB86F \uCD5C\uC18C \uC218\uB294 \uCD5C\uB300 \uC218\uBCF4\uB2E4 \uD074 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2287
2410
  for (let index = 0; index < (slot.multiple?.max ?? 1); index++) {
2288
2411
  const path = slot.multiple ? slot.multiple.path.replace("{n}", String(index)) : slot.path;
2289
- if (paths.has(path)) fail("/output/asset_slots", "\uC2AC\uB86F \uD30C\uC77C \uACBD\uB85C\uB294 \uC911\uBCF5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2412
+ if (paths.has(path)) fail2("/output/asset_slots", "\uC2AC\uB86F \uD30C\uC77C \uACBD\uB85C\uB294 \uC911\uBCF5\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2290
2413
  paths.add(path);
2291
2414
  }
2292
2415
  }
2416
+ inspectIntroSemantics(skill, form, slots, fail2);
2293
2417
  if (!form) return errors;
2294
2418
  const properties = form.properties ?? {};
2295
2419
  const exists = (expression, path) => {
2296
2420
  const match = /^([a-z][a-z0-9_-]*)(?:\[(\d+)\])?$/.exec(expression);
2297
2421
  if (!match || !Object.hasOwn(properties, match[1])) {
2298
- fail(path, "\uC120\uC5B8\uD55C \uD3FC \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
2422
+ fail2(path, "\uC120\uC5B8\uD55C \uD3FC \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
2299
2423
  return;
2300
2424
  }
2301
2425
  if (match[2] !== void 0) {
2302
2426
  const slot = slots.find((value2) => value2.field === match[1]);
2303
- if (!slot?.multiple || Number(match[2]) >= slot.multiple.max) fail(path, "\uB2E4\uC911 \uC774\uBBF8\uC9C0 \uC2AC\uB86F\uC758 \uC720\uD6A8\uD55C \uC778\uB371\uC2A4\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
2427
+ if (!slot?.multiple || Number(match[2]) >= slot.multiple.max) fail2(path, "\uB2E4\uC911 \uC774\uBBF8\uC9C0 \uC2AC\uB86F\uC758 \uC720\uD6A8\uD55C \uC778\uB371\uC2A4\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
2304
2428
  }
2305
2429
  };
2306
2430
  const entity = skill.output?.entity_map;
@@ -2310,30 +2434,539 @@ function inspectAuthoringSemantics(skill, form) {
2310
2434
  }
2311
2435
  for (const slot of slots) {
2312
2436
  const node = properties[slot.field];
2313
- if (!node || node["x-clack-widget"] !== (slot.multiple ? "image_list" : "image")) fail("/output/asset_slots", "\uC2AC\uB86F\uACFC \uC774\uBBF8\uC9C0 \uC704\uC82F \uC885\uB958\uAC00 \uC77C\uCE58\uD574\uC57C \uD569\uB2C8\uB2E4.");
2437
+ if (!node || node["x-clack-widget"] !== (slot.multiple ? "image_list" : "image")) fail2("/output/asset_slots", "\uC2AC\uB86F\uACFC \uC774\uBBF8\uC9C0 \uC704\uC82F \uC885\uB958\uAC00 \uC77C\uCE58\uD574\uC57C \uD569\uB2C8\uB2E4.");
2314
2438
  }
2315
2439
  for (const [name2, node] of Object.entries(properties)) {
2316
2440
  if (node["x-clack-widget"] === "content_ref") {
2317
- if (node.type !== "string" || !node["x-clack-source"]) fail(`/properties/${name2}`, "content_ref\uC5D0\uB294 \uBB38\uC790\uC5F4\uACFC x-clack-source \uC120\uC5B8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
2441
+ if (node.type !== "string" || !node["x-clack-source"]) fail2(`/properties/${name2}`, "content_ref\uC5D0\uB294 \uBB38\uC790\uC5F4\uACFC x-clack-source \uC120\uC5B8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
2318
2442
  for (const target of Object.keys(node["x-clack-source"]?.import ?? {})) {
2319
2443
  exists(target, `/properties/${name2}/x-clack-source/import`);
2320
- if (properties[target]?.["x-clack-widget"] === "content_ref") fail(`/properties/${name2}`, "\uB2E4\uB978 content_ref \uD544\uB4DC\uB85C \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2444
+ if (properties[target]?.["x-clack-widget"] === "content_ref") fail2(`/properties/${name2}`, "\uB2E4\uB978 content_ref \uD544\uB4DC\uB85C \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2321
2445
  }
2322
- } else if (node["x-clack-source"]) fail(`/properties/${name2}`, "x-clack-source\uB294 content_ref\uC5D0\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4.");
2323
- if (node["x-clack-widget"] === "image_list" && (node.type !== "array" || node.items?.type !== "object" || !node["x-clack-asset"])) fail(`/properties/${name2}`, "image_list\uC5D0\uB294 \uAC1D\uCCB4 \uBC30\uC5F4\uACFC \uC790\uC0B0 \uC120\uC5B8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
2446
+ } else if (node["x-clack-source"]) fail2(`/properties/${name2}`, "x-clack-source\uB294 content_ref\uC5D0\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4.");
2447
+ if (node["x-clack-widget"] === "image_list" && (node.type !== "array" || node.items?.type !== "object" || !node["x-clack-asset"])) fail2(`/properties/${name2}`, "image_list\uC5D0\uB294 \uAC1D\uCCB4 \uBC30\uC5F4\uACFC \uC790\uC0B0 \uC120\uC5B8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
2324
2448
  }
2325
2449
  return errors;
2326
2450
  }
2327
2451
 
2328
2452
  // src/lib/skill-package.ts
2329
- import { createHash as createHash3 } from "crypto";
2453
+ import { createHash as createHash4 } from "crypto";
2330
2454
  import { lstat as lstat4, open as open3, readdir } from "fs/promises";
2331
2455
  import { constants } from "fs";
2332
2456
  import { basename as basename4, extname as extname2, join as join2, resolve as resolve7 } from "path";
2333
- import { Ajv2020 } from "ajv/dist/2020.js";
2457
+ import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
2334
2458
  import { unzipSync as unzipSync2, zipSync } from "fflate";
2335
2459
  import { parseDocument } from "yaml";
2336
2460
 
2461
+ // src/lib/skill-editor.ts
2462
+ import { createHash as createHash3 } from "crypto";
2463
+ import { Ajv2020 } from "ajv/dist/2020.js";
2464
+
2465
+ // src/vendor/clack-api-v4-contracts/output-data-inspect.mjs
2466
+ var bytes = (value2) => Buffer.byteLength(JSON.stringify(value2), "utf8");
2467
+ var fail = (path, message) => ({ path, message });
2468
+ var OUTPUT_DATA_LIMITS = { schema_bytes: 65536, depth: 6, properties: 400, public_bytes: 262144, private_bytes: 65536, ui_state_bytes: 262144 };
2469
+ var SCALAR_TYPES = /* @__PURE__ */ new Set(["string", "integer", "number", "boolean"]);
2470
+ var SCALAR_KEYS = {
2471
+ string: ["minLength", "maxLength", "format"],
2472
+ number: ["minimum", "maximum"],
2473
+ integer: ["minimum", "maximum"],
2474
+ array: ["items", "minItems", "maxItems", "uniqueItems"],
2475
+ object: ["properties", "required", "additionalProperties"],
2476
+ boolean: []
2477
+ };
2478
+ var TYPED_KEYS = new Set(Object.values(SCALAR_KEYS).flat());
2479
+ function inspectOutputData(schema) {
2480
+ const errors = [];
2481
+ if (bytes(schema) > OUTPUT_DATA_LIMITS.schema_bytes) errors.push(fail("/", "\uB370\uC774\uD130 \uC2A4\uD0A4\uB9C8\uB294 64 KiB \uC774\uD558\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4."));
2482
+ for (const name2 of schema.required ?? []) if (!Object.hasOwn(schema.properties, name2)) errors.push(fail("/required", `\uC120\uC5B8\uD558\uC9C0 \uC54A\uC740 \uB8E8\uD2B8 \uD0A4 ${name2}\uC785\uB2C8\uB2E4.`));
2483
+ if (!Object.values(schema.properties).some((field2) => field2["x-clack-visibility"] === "public")) errors.push(fail("/properties", "\uACF5\uAC1C \uB8E8\uD2B8 \uD0A4\uAC00 \uD558\uB098 \uC774\uC0C1 \uD544\uC694\uD569\uB2C8\uB2E4."));
2484
+ const defs = schema.$defs ?? {};
2485
+ const resolve9 = (node) => typeof node?.$ref === "string" ? defs[node.$ref.slice("#/$defs/".length)] : node;
2486
+ let properties = 0;
2487
+ const suggestRoots = [];
2488
+ const assetPointers = [];
2489
+ walkDataSchema(schema, (node, info) => {
2490
+ const at = info.pointer || "/";
2491
+ if (info.cycle) {
2492
+ errors.push(fail(at, "\uC21C\uD658 \uCC38\uC870\uB294 \uD5C8\uC6A9\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."));
2493
+ return;
2494
+ }
2495
+ if (info.unresolved) {
2496
+ errors.push(fail(at, "\uB85C\uCEEC $defs \uCC38\uC870\uB9CC \uD5C8\uC6A9\uD569\uB2C8\uB2E4."));
2497
+ return;
2498
+ }
2499
+ if (info.depth > OUTPUT_DATA_LIMITS.depth) errors.push(fail(at, "\uB370\uC774\uD130 \uBB38\uC11C \uAE4A\uC774\uB294 \uB8E8\uD2B8 \uC544\uB798 6 \uC774\uD558\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4."));
2500
+ if (info.depth === 0) return;
2501
+ if (typeof node.$ref === "string") {
2502
+ if (Object.keys(node).some((key) => !["$ref", "title", "description", "x-clack-i18n", "x-clack-visibility"].includes(key))) errors.push(fail(at, "$ref\uC5D0\uB294 \uC124\uBA85 \uD0A4\uB9CC \uD568\uAED8 \uB458 \uC218 \uC788\uC2B5\uB2C8\uB2E4."));
2503
+ return;
2504
+ }
2505
+ if (!info.branch) properties++;
2506
+ if (Array.isArray(node.oneOf)) {
2507
+ const discriminator = node["x-clack-discriminator"];
2508
+ if (!discriminator) errors.push(fail(at, "oneOf\uC5D0\uB294 x-clack-discriminator\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."));
2509
+ if (node.type || node.properties || node.items || node.enum || Object.hasOwn(node, "const")) errors.push(fail(at, "oneOf \uB178\uB4DC\uC5D0\uB294 \uD615\uC2DD \uD0A4\uB97C \uD568\uAED8 \uB458 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
2510
+ const seen = /* @__PURE__ */ new Set();
2511
+ for (const raw of node.oneOf) {
2512
+ const branch = resolve9(raw);
2513
+ const tag = branch?.properties?.[discriminator];
2514
+ if (branch?.type !== "object" || typeof tag?.const !== "string" || tag.type !== "string" || !(branch.required ?? []).includes(discriminator)) {
2515
+ errors.push(fail(at, "\uAC01 oneOf \uBD84\uAE30\uB294 \uD310\uBCC4\uC790 \uBB38\uC790\uC5F4 const\uB97C \uD544\uC218\uB85C \uAC00\uC9C4 \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4."));
2516
+ } else if (seen.has(tag.const)) errors.push(fail(at, `\uD310\uBCC4\uC790 \uAC12 ${tag.const}\uC774 \uC911\uBCF5\uB429\uB2C8\uB2E4.`));
2517
+ else seen.add(tag.const);
2518
+ }
2519
+ return;
2520
+ }
2521
+ if (node["x-clack-discriminator"] !== void 0) errors.push(fail(at, "x-clack-discriminator\uB294 oneOf\uC5D0\uB9CC \uB461\uB2C8\uB2E4."));
2522
+ if (!node.type) {
2523
+ errors.push(fail(at, "type\uC774 \uD544\uC694\uD569\uB2C8\uB2E4."));
2524
+ return;
2525
+ }
2526
+ for (const key of Object.keys(node)) if (TYPED_KEYS.has(key) && !SCALAR_KEYS[node.type].includes(key)) errors.push(fail(`${at}/${key}`, `${node.type}\uC5D0 \uC4F8 \uC218 \uC5C6\uB294 \uD0A4\uC785\uB2C8\uB2E4.`));
2527
+ const range = (low, high, label) => {
2528
+ if (typeof node[low] === "number" && typeof node[high] === "number" && node[low] > node[high]) errors.push(fail(at, `${label} \uBC94\uC704\uAC00 \uC798\uBABB\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`));
2529
+ };
2530
+ range("minLength", "maxLength", "\uAE38\uC774");
2531
+ range("minItems", "maxItems", "\uD56D\uBAA9 \uC218");
2532
+ range("minimum", "maximum", "\uAC12");
2533
+ if (node.type === "string" && typeof node.maxLength !== "number") errors.push(fail(at, "\uBB38\uC790\uC5F4 maxLength\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."));
2534
+ if (node.type === "array" && (!node.items || typeof node.maxItems !== "number")) errors.push(fail(at, "\uBC30\uC5F4\uC5D0\uB294 items\uC640 maxItems\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."));
2535
+ if (node.type === "array" && node.uniqueItems === true) {
2536
+ let items = node.items;
2537
+ for (let hops = 0; typeof items?.$ref === "string" && hops < 64; hops++) items = resolve9(items);
2538
+ if (!items || Array.isArray(items.oneOf) || !SCALAR_TYPES.has(items.type)) errors.push(fail(`${at}/uniqueItems`, "uniqueItems\uB294 \uC2A4\uCE7C\uB77C \uBC30\uC5F4\uC5D0\uB9CC \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4."));
2539
+ }
2540
+ if (node.type === "object") {
2541
+ if (node.additionalProperties !== false || !node.properties || !Object.keys(node.properties).length) errors.push(fail(at, "\uAC1D\uCCB4\uC5D0\uB294 properties\uC640 additionalProperties: false\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."));
2542
+ for (const name2 of node.required ?? []) if (!Object.hasOwn(node.properties ?? {}, name2)) errors.push(fail(at, `\uC120\uC5B8\uD558\uC9C0 \uC54A\uC740 \uD0A4 ${name2}\uC744 required\uC5D0 \uB450\uC5C8\uC2B5\uB2C8\uB2E4.`));
2543
+ }
2544
+ for (const value2 of node.enum ?? []) if (typeof value2 !== (node.type === "integer" ? "number" : node.type)) errors.push(fail(at, "enum \uAC12\uC758 \uD615\uC2DD\uC774 type\uACFC \uB2E4\uB985\uB2C8\uB2E4."));
2545
+ if (node["x-clack-asset"]) {
2546
+ assetPointers.push(at);
2547
+ if (node.type !== "string" || node.format !== "uuid" || node.maxLength !== 36 || node.enum) errors.push(fail(at, "\uC790\uC0B0 \uB178\uB4DC\uB294 {type:string, format:uuid, maxLength:36}\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4."));
2548
+ if (info.visibility === "private") errors.push(fail(at, "\uBE44\uACF5\uAC1C \uD0A4\uC5D0\uB294 \uC790\uC0B0\uC744 \uB458 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
2549
+ } else if (node.format === "uuid" && node.type !== "string") errors.push(fail(at, "format\uC740 \uBB38\uC790\uC5F4\uC5D0\uB9CC \uC501\uB2C8\uB2E4."));
2550
+ if (node["x-clack-ai"]?.suggest === true) {
2551
+ if (info.visibility !== "public") errors.push(fail(at, "AI \uC81C\uC548\uC740 \uACF5\uAC1C \uD0A4\uC5D0\uB9CC \uCF24 \uC218 \uC788\uC2B5\uB2C8\uB2E4."));
2552
+ suggestRoots.push(at);
2553
+ }
2554
+ });
2555
+ if (properties > OUTPUT_DATA_LIMITS.properties) errors.push(fail("/", "\uB370\uC774\uD130 \uC2A4\uD0A4\uB9C8 \uC18D\uC131\uC740 400\uAC1C \uC774\uD558\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4."));
2556
+ for (const root of suggestRoots) if (assetPointers.some((asset) => asset === root || asset.startsWith(`${root}/`))) errors.push(fail(root, "AI \uC81C\uC548 \uD558\uC704 \uD2B8\uB9AC\uC5D0 \uC790\uC0B0 \uB178\uB4DC\uB97C \uB458 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
2557
+ return errors;
2558
+ }
2559
+ var ZWJ_AFTER = new RegExp("\\p{Extended_Pictographic}", "u");
2560
+
2561
+ // src/vendor/clack-api-v4-contracts/schemas/output-data.v1.schema.json
2562
+ var output_data_v1_schema_default = {
2563
+ $schema: "https://json-schema.org/draft/2020-12/schema",
2564
+ $id: "https://clack.kr/schemas/platform/output-data.v1.schema.json",
2565
+ $comment: "\uBAA8\uB178\uB808\uD3EC clack-api-v4/contracts/platform/schemas/output-data.v1.schema.json\uC5D0\uC11C \uBCF5\uC0AC\uD55C \uD30C\uC77C\uC774\uB2E4. \uC774 \uD30C\uC77C\uC744 \uC9C1\uC811 \uC218\uC815\uD558\uC9C0 \uB9D0\uACE0 scripts/sync-vendor-types.mjs\uB85C \uAC31\uC2E0\uD55C\uB2E4.",
2566
+ title: "\uC2A4\uD0AC \uC5D0\uB514\uD130 \uB370\uC774\uD130 \uBB38\uC11C \uC2A4\uD0A4\uB9C8 \uADDC\uCE59(output-data.v1)",
2567
+ description: "\uC2A4\uD0AC \uB9E4\uB2C8\uD398\uC2A4\uD2B8 output.data.schema\uAC00 \uAC00\uB9AC\uD0A4\uB294 \uB370\uC774\uD130 \uBB38\uC11C \uC2A4\uD0A4\uB9C8 \uD30C\uC77C\uC774 \uB530\uB77C\uC57C \uD558\uB294 \uBA54\uD0C0 \uADDC\uCE59\uC774\uB2E4. v1\uC740 pattern\uC744 \uD5C8\uC6A9\uD558\uC9C0 \uC54A\uACE0(\uBC31\uD2B8\uB798\uD0B9 \uBE44\uC6A9), uniqueItems\uB294 \uC2A4\uCE7C\uB77C \uBC30\uC5F4\uC5D0\uB9CC \uC4F4\uB2E4. \uAE4A\uC774 6\xB7\uBB38\uC790\uC5F4 maxLength \uD544\uC218\xB7\uBC30\uC5F4 maxItems \uD544\uC218\xB7\uD310\uBCC4\uC790 oneOf\xB7\uB85C\uCEEC $ref\uB9CC\xB7\uC21C\uD658 \uAE08\uC9C0\xB7\uBE44\uACF5\uAC1C\uB294 \uCD5C\uC0C1\uC704 \uD0A4\uB9CC\xB7uniqueItems \uB300\uC0C1\uCC98\uB7FC \uAC19\uC740 \uC2A4\uD0A4\uB9C8\uB85C \uD45C\uD604\uD560 \uC218 \uC5C6\uB294 \uADDC\uCE59\uC740 validators.mjs\uC758 inspectOutputData\uAC00 \uCD94\uAC00\uB85C \uAC80\uC0AC\uD55C\uB2E4.",
2568
+ type: "object",
2569
+ additionalProperties: false,
2570
+ properties: {
2571
+ $schema: {
2572
+ const: "https://json-schema.org/draft/2020-12/schema"
2573
+ },
2574
+ title: {
2575
+ type: "string",
2576
+ maxLength: 120
2577
+ },
2578
+ description: {
2579
+ type: "string",
2580
+ maxLength: 1e3
2581
+ },
2582
+ type: {
2583
+ const: "object"
2584
+ },
2585
+ additionalProperties: {
2586
+ const: false
2587
+ },
2588
+ properties: {
2589
+ type: "object",
2590
+ propertyNames: {
2591
+ $ref: "#/$defs/key"
2592
+ },
2593
+ additionalProperties: {
2594
+ $ref: "#/$defs/rootField"
2595
+ },
2596
+ minProperties: 1,
2597
+ maxProperties: 60
2598
+ },
2599
+ required: {
2600
+ $ref: "#/$defs/keyList"
2601
+ },
2602
+ $defs: {
2603
+ type: "object",
2604
+ propertyNames: {
2605
+ type: "string",
2606
+ pattern: "^[A-Za-z][A-Za-z0-9_-]{0,63}$"
2607
+ },
2608
+ additionalProperties: {
2609
+ $ref: "#/$defs/node"
2610
+ },
2611
+ maxProperties: 64
2612
+ }
2613
+ },
2614
+ required: [
2615
+ "type",
2616
+ "properties",
2617
+ "additionalProperties"
2618
+ ],
2619
+ $defs: {
2620
+ key: {
2621
+ type: "string",
2622
+ pattern: "^[a-z][a-z0-9_]{0,31}$"
2623
+ },
2624
+ keyList: {
2625
+ type: "array",
2626
+ items: {
2627
+ $ref: "#/$defs/key"
2628
+ },
2629
+ uniqueItems: true
2630
+ },
2631
+ label: {
2632
+ type: "object",
2633
+ additionalProperties: false,
2634
+ properties: {
2635
+ ko: {
2636
+ type: "string",
2637
+ minLength: 1,
2638
+ maxLength: 200
2639
+ },
2640
+ en: {
2641
+ type: "string",
2642
+ minLength: 1,
2643
+ maxLength: 200
2644
+ }
2645
+ },
2646
+ required: [
2647
+ "ko"
2648
+ ]
2649
+ },
2650
+ asset: {
2651
+ type: "object",
2652
+ additionalProperties: false,
2653
+ properties: {
2654
+ accept: {
2655
+ type: "array",
2656
+ items: {
2657
+ type: "string",
2658
+ enum: [
2659
+ "image/png",
2660
+ "image/jpeg",
2661
+ "image/webp"
2662
+ ]
2663
+ },
2664
+ minItems: 1,
2665
+ uniqueItems: true
2666
+ },
2667
+ max_bytes: {
2668
+ type: "integer",
2669
+ minimum: 1,
2670
+ maximum: 4194304
2671
+ },
2672
+ aspect: {
2673
+ type: "string",
2674
+ pattern: "^[1-9][0-9]*:[1-9][0-9]*$"
2675
+ },
2676
+ generate: {
2677
+ type: "boolean"
2678
+ }
2679
+ },
2680
+ required: [
2681
+ "accept",
2682
+ "max_bytes"
2683
+ ]
2684
+ },
2685
+ ai: {
2686
+ type: "object",
2687
+ additionalProperties: false,
2688
+ properties: {
2689
+ suggest: {
2690
+ type: "boolean"
2691
+ },
2692
+ hint: {
2693
+ type: "string",
2694
+ maxLength: 300
2695
+ }
2696
+ },
2697
+ required: [
2698
+ "suggest"
2699
+ ]
2700
+ },
2701
+ scalar: {
2702
+ type: [
2703
+ "string",
2704
+ "number",
2705
+ "boolean"
2706
+ ]
2707
+ },
2708
+ nodeKeywords: {
2709
+ type: "object",
2710
+ properties: {
2711
+ type: {
2712
+ type: "string",
2713
+ enum: [
2714
+ "object",
2715
+ "array",
2716
+ "string",
2717
+ "integer",
2718
+ "number",
2719
+ "boolean"
2720
+ ]
2721
+ },
2722
+ title: {
2723
+ type: "string",
2724
+ maxLength: 120
2725
+ },
2726
+ description: {
2727
+ type: "string",
2728
+ maxLength: 1e3
2729
+ },
2730
+ "x-clack-i18n": {
2731
+ $ref: "#/$defs/label"
2732
+ },
2733
+ enum: {
2734
+ type: "array",
2735
+ items: {
2736
+ $ref: "#/$defs/scalar"
2737
+ },
2738
+ minItems: 1,
2739
+ maxItems: 100,
2740
+ uniqueItems: true
2741
+ },
2742
+ const: {
2743
+ $ref: "#/$defs/scalar"
2744
+ },
2745
+ minLength: {
2746
+ type: "integer",
2747
+ minimum: 0
2748
+ },
2749
+ maxLength: {
2750
+ type: "integer",
2751
+ minimum: 1,
2752
+ maximum: 1e5
2753
+ },
2754
+ format: {
2755
+ const: "uuid"
2756
+ },
2757
+ minimum: {
2758
+ type: "number"
2759
+ },
2760
+ maximum: {
2761
+ type: "number"
2762
+ },
2763
+ minItems: {
2764
+ type: "integer",
2765
+ minimum: 0
2766
+ },
2767
+ maxItems: {
2768
+ type: "integer",
2769
+ minimum: 1,
2770
+ maximum: 1e3
2771
+ },
2772
+ uniqueItems: {
2773
+ type: "boolean",
2774
+ description: "\uC2A4\uCE7C\uB77C(string\xB7integer\xB7number\xB7boolean) items \uBC30\uC5F4\uC5D0\uB9CC \uC4F4\uB2E4(validators.mjs \uAC80\uC0AC)."
2775
+ },
2776
+ items: {
2777
+ $ref: "#/$defs/node"
2778
+ },
2779
+ properties: {
2780
+ type: "object",
2781
+ propertyNames: {
2782
+ $ref: "#/$defs/key"
2783
+ },
2784
+ additionalProperties: {
2785
+ $ref: "#/$defs/node"
2786
+ },
2787
+ maxProperties: 60
2788
+ },
2789
+ required: {
2790
+ $ref: "#/$defs/keyList"
2791
+ },
2792
+ additionalProperties: {
2793
+ const: false
2794
+ },
2795
+ $ref: {
2796
+ type: "string",
2797
+ pattern: "^#/\\$defs/[A-Za-z][A-Za-z0-9_-]{0,63}$"
2798
+ },
2799
+ oneOf: {
2800
+ type: "array",
2801
+ items: {
2802
+ $ref: "#/$defs/node"
2803
+ },
2804
+ minItems: 2,
2805
+ maxItems: 16
2806
+ },
2807
+ "x-clack-discriminator": {
2808
+ $ref: "#/$defs/key"
2809
+ },
2810
+ "x-clack-asset": {
2811
+ $ref: "#/$defs/asset"
2812
+ },
2813
+ "x-clack-ai": {
2814
+ $ref: "#/$defs/ai"
2815
+ }
2816
+ }
2817
+ },
2818
+ node: {
2819
+ allOf: [
2820
+ {
2821
+ $ref: "#/$defs/nodeKeywords"
2822
+ }
2823
+ ],
2824
+ unevaluatedProperties: false
2825
+ },
2826
+ rootField: {
2827
+ allOf: [
2828
+ {
2829
+ $ref: "#/$defs/nodeKeywords"
2830
+ }
2831
+ ],
2832
+ properties: {
2833
+ "x-clack-visibility": {
2834
+ type: "string",
2835
+ enum: [
2836
+ "public",
2837
+ "private"
2838
+ ]
2839
+ }
2840
+ },
2841
+ required: [
2842
+ "x-clack-visibility"
2843
+ ],
2844
+ unevaluatedProperties: false
2845
+ }
2846
+ }
2847
+ };
2848
+
2849
+ // src/lib/skill-editor.ts
2850
+ var EDITOR_BUNDLE_LIMITS = { maxFiles: 50, maxFileBytes: 512 * 1024, maxTotalBytes: 2 * 1024 * 1024, maxCodeBytes: 512 * 1024 };
2851
+ var DATA_SCHEMA_MAX_BYTES = 64 * 1024;
2852
+ var ALLOWED = /\.(?:html?|js|mjs|css|json|png|jpe?g|webp|gif|woff2?)$/i;
2853
+ var CODE = /\.(?:html?|js|mjs|css)$/i;
2854
+ var RESERVED = /^(?:runtime|form|examples|output|data)(?:\/|$)/;
2855
+ var textDecoder = new TextDecoder("utf-8", { fatal: true });
2856
+ var validateMeta = new Ajv2020({ allErrors: true, strict: false }).compile(output_data_v1_schema_default);
2857
+ var invalid = (message) => {
2858
+ throw new CliError(message, "SKILL_EDITOR_INVALID");
2859
+ };
2860
+ var isObject2 = (value2) => value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
2861
+ var sha = (value2) => createHash3("sha256").update(value2).digest("hex");
2862
+ function selectEditorBundle(files, bundle) {
2863
+ const single = Object.hasOwn(files, bundle);
2864
+ const prefix = `${bundle}/`;
2865
+ const selected = single ? [[bundle, files[bundle]]] : Object.entries(files).filter(([path]) => path.startsWith(prefix));
2866
+ const list2 = selected.map(([path, data]) => ({ path: single ? path.split("/").at(-1) : path.slice(prefix.length), bytes: data.length, sha256: sha(data) })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
2867
+ const computed = single ? list2[0]?.sha256 ?? "" : sha(JSON.stringify(list2));
2868
+ return { single, selected, list: list2, computed };
2869
+ }
2870
+ function computeEditorHash(manifest, files) {
2871
+ const editor = manifest.authoring?.editor;
2872
+ if (!isObject2(editor) || typeof editor.bundle !== "string") return null;
2873
+ const { list: list2, computed } = selectEditorBundle(files, editor.bundle.replace(/\/+$/, ""));
2874
+ return list2.length ? computed : null;
2875
+ }
2876
+ function parseJson(bytes2, label) {
2877
+ try {
2878
+ const value2 = JSON.parse(textDecoder.decode(bytes2));
2879
+ if (isObject2(value2)) return value2;
2880
+ } catch {
2881
+ }
2882
+ return invalid(`${label}\uC740 UTF-8 JSON \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
2883
+ }
2884
+ function scanBundle(list2, warnings) {
2885
+ for (const [path, data] of list2) {
2886
+ if (!/\.(?:html?|js|mjs|css)$/i.test(path)) continue;
2887
+ let text;
2888
+ try {
2889
+ text = textDecoder.decode(data);
2890
+ } catch {
2891
+ invalid(`${path}\uB294 UTF-8 \uD14D\uC2A4\uD2B8\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
2892
+ return;
2893
+ }
2894
+ if (/loading\s*=\s*["']?lazy/i.test(text)) warnings.push(`EDITOR_LAZY_LOAD: ${path}\uC5D0 loading="lazy"\uAC00 \uC788\uC2B5\uB2C8\uB2E4. \uC5D0\uB514\uD130\uB294 \uC2DC\uC791\uD560 \uB54C \uBAA8\uB4E0 \uC790\uC6D0\uC744 \uBD88\uB7EC\uC640\uC57C \uD569\uB2C8\uB2E4.`);
2895
+ if (/\bimport\s*\(/.test(text)) warnings.push(`EDITOR_LAZY_LOAD: ${path}\uC5D0 \uB3D9\uC801 import()\uAC00 \uC788\uC2B5\uB2C8\uB2E4. \uC815\uC801 import\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4.`);
2896
+ if (/\.css$/i.test(path) && /url\(\s*(?!["']?data:)/i.test(text)) {
2897
+ warnings.push(`EDITOR_CSS_URL_NOT_INLINE: ${path}\uC758 CSS url()\uC740 data:\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4. \uAE00\uAF34\xB7\uBC30\uACBD \uC774\uBBF8\uC9C0\uB294 \uC778\uB77C\uC778\uD558\uC138\uC694.`);
2898
+ }
2899
+ }
2900
+ }
2901
+ function inspectEditorSkill(manifest, files, options2) {
2902
+ const authoring = manifest.authoring;
2903
+ const output2 = manifest.output;
2904
+ const editor = authoring.editor;
2905
+ const warnings = [];
2906
+ const bundle = String(editor.bundle).replace(/\/+$/, "");
2907
+ if (RESERVED.test(bundle)) invalid("editor \uBC88\uB4E4 \uACBD\uB85C\uAC00 \uC608\uC57D \uACBD\uB85C(runtime\xB7form\xB7examples\xB7output\xB7data)\uC785\uB2C8\uB2E4.");
2908
+ const { single, selected, list: list2, computed } = selectEditorBundle(files, bundle);
2909
+ if (!list2.length) invalid(`editor \uBC88\uB4E4(${bundle})\uC5D0 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`);
2910
+ const entry = single ? bundle : `${bundle}/index.html`;
2911
+ if (single ? !/\.html?$/i.test(bundle) : !list2.some((file) => file.path === "index.html")) invalid(`editor \uC9C4\uC785\uC810(${entry})\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`);
2912
+ if (list2.length > EDITOR_BUNDLE_LIMITS.maxFiles) invalid(`editor \uBC88\uB4E4 \uD30C\uC77C \uC218\uAC00 ${EDITOR_BUNDLE_LIMITS.maxFiles}\uAC1C\uB97C \uB118\uC2B5\uB2C8\uB2E4.`);
2913
+ const total = list2.reduce((sum, file) => sum + file.bytes, 0);
2914
+ if (total > EDITOR_BUNDLE_LIMITS.maxTotalBytes) invalid("editor \uBC88\uB4E4 \uC804\uCCB4 \uD06C\uAE30\uAC00 2 MiB\uB97C \uB118\uC2B5\uB2C8\uB2E4.");
2915
+ let code = 0;
2916
+ for (const [path, data2] of selected) {
2917
+ if (!ALLOWED.test(path)) invalid(`editor \uBC88\uB4E4\uC5D0 \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${path}`);
2918
+ if (data2.length > EDITOR_BUNDLE_LIMITS.maxFileBytes) invalid(`editor \uBC88\uB4E4 \uD30C\uC77C\uC774 512 KiB\uB97C \uB118\uC2B5\uB2C8\uB2E4: ${path}`);
2919
+ if (CODE.test(path)) code += data2.length;
2920
+ }
2921
+ if (code > EDITOR_BUNDLE_LIMITS.maxCodeBytes) invalid("editor \uBC88\uB4E4 \uCF54\uB4DC(HTML\xB7JS\xB7CSS) \uD569\uACC4\uAC00 512 KiB\uB97C \uB118\uC2B5\uB2C8\uB2E4.");
2922
+ scanBundle(selected, warnings);
2923
+ const declared = String(editor.sha256);
2924
+ const match = declared === computed;
2925
+ if (!match) {
2926
+ const message = `authoring.editor.sha256\uC774 \uBC88\uB4E4 \uD574\uC2DC\uC640 \uB2E4\uB985\uB2C8\uB2E4(\uACC4\uC0B0\uAC12 ${computed}). \`clack skill pack\`\uC774 \uAC12\uC744 \uCC44\uC6C1\uB2C8\uB2E4.`;
2927
+ if (options2.hashMismatch === "error") invalid(message);
2928
+ warnings.push(`PLUGIN_BUNDLE_HASH_MISMATCH: ${message}`);
2929
+ }
2930
+ const data = output2.data;
2931
+ const schemaPath = data?.schema;
2932
+ if (typeof schemaPath !== "string" || !files[schemaPath]) return invalid("output.data.schema \uD30C\uC77C\uC774 \uD328\uD0A4\uC9C0\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4.");
2933
+ if (files[schemaPath].length > DATA_SCHEMA_MAX_BYTES) invalid("\uB370\uC774\uD130 \uC2A4\uD0A4\uB9C8 \uD30C\uC77C\uC740 64 KiB \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
2934
+ const schema = parseJson(files[schemaPath], "\uB370\uC774\uD130 \uC2A4\uD0A4\uB9C8");
2935
+ if (!validateMeta(schema)) {
2936
+ invalid(`\uB370\uC774\uD130 \uC2A4\uD0A4\uB9C8\uAC00 output-data.v1\uACFC \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${(validateMeta.errors ?? []).slice(0, 5).map((error2) => `${error2.instancePath || "/"} ${error2.message ?? ""}`).join(" / ")}`);
2937
+ }
2938
+ const dataErrors = [...inspectOutputData(schema), ...inspectAuthoringSemantics(manifest, void 0, schema)];
2939
+ if (dataErrors.length) invalid(dataErrors.map((error2) => `${error2.path}: ${error2.message}`).join(" / "));
2940
+ const template = output2.template;
2941
+ if (!template || template.root !== "runtime" || typeof template.entry !== "string") invalid("runtime \uD15C\uD50C\uB9BF \uACBD\uB85C\uB97C \uD655\uC778\uD558\uC138\uC694.");
2942
+ if (Object.keys(files).some((path) => path.startsWith("runtime/data/"))) invalid("runtime/data/ \uACBD\uB85C\uB294 \uC5D0\uB514\uD130 \uC2A4\uD0AC\uC5D0\uC11C \uC608\uC57D\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4(data/document.json\uACFC \uCDA9\uB3CC).");
2943
+ const examples = Object.keys(files).filter((path) => /^examples\/[^/]+\.json$/.test(path));
2944
+ if (!examples.length || examples.length > 10) invalid("\uD15C\uD50C\uB9BF \uC608\uC2DC\uB294 1~10\uAC1C \uD544\uC694\uD569\uB2C8\uB2E4.");
2945
+ let validateExample;
2946
+ try {
2947
+ const ajv2 = new Ajv2020({ allErrors: true, strict: false });
2948
+ ajv2.addFormat("uuid", /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
2949
+ validateExample = ajv2.compile(schema);
2950
+ } catch {
2951
+ return invalid("\uB370\uC774\uD130 \uC2A4\uD0A4\uB9C8\uB97C \uCEF4\uD30C\uC77C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2952
+ }
2953
+ for (const path of examples) {
2954
+ if (files[path].length > 256 * 1024) invalid(`\uC608\uC2DC\uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4: ${path}`);
2955
+ if (!validateExample(parseJson(files[path], "\uC608\uC2DC"))) invalid(`\uB370\uC774\uD130 \uC2A4\uD0A4\uB9C8\uC640 \uC608\uC2DC\uAC00 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${path}`);
2956
+ }
2957
+ return { computedHash: computed, warnings, summary: {
2958
+ bundle,
2959
+ entry,
2960
+ file_count: list2.length,
2961
+ total_bytes: total,
2962
+ declared_sha256: declared,
2963
+ computed_sha256: computed,
2964
+ hash_match: match,
2965
+ permissions: editor.permissions,
2966
+ center_only: true
2967
+ } };
2968
+ }
2969
+
2337
2970
  // src/vendor/clack-api-v4-contracts/schemas/clack-skill.v1.schema.json
2338
2971
  var clack_skill_v1_schema_default = {
2339
2972
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -2624,6 +3257,64 @@ var clack_skill_v1_schema_default = {
2624
3257
  "permissions",
2625
3258
  "purpose"
2626
3259
  ]
3260
+ },
3261
+ editor: {
3262
+ type: "object",
3263
+ additionalProperties: false,
3264
+ description: "\uC2A4\uD0AC \uC5D0\uB514\uD130(\uC804\uCCB4 \uD654\uBA74 \uC81C\uC791 UI) \uC120\uC5B8. v1\uC740 template \uC2A4\uD0AC \uC804\uC6A9\uC774\uBA70 \uC81C\uC791 \uD3FC\xB7\uD50C\uB7EC\uADF8\uC778 \uD328\uB110\uACFC \uD568\uAED8 \uC4F8 \uC218 \uC5C6\uB2E4. \uBC88\uB4E4 \uADDC\uCE59(\uB514\uB809\uD130\uB9AC\uBA74 {bundle}/index.html \uC9C4\uC785, sha256 \uACC4\uC0B0\uBC95)\uC740 authoring.plugin\uACFC \uAC19\uB2E4.",
3265
+ properties: {
3266
+ bundle: {
3267
+ type: "string",
3268
+ pattern: "^(?!/)(?!.*\\.\\.)[A-Za-z0-9_./-]+$"
3269
+ },
3270
+ sha256: {
3271
+ type: "string",
3272
+ pattern: "^[a-f0-9]{64}$"
3273
+ },
3274
+ permissions: {
3275
+ type: "array",
3276
+ items: {
3277
+ type: "string",
3278
+ enum: [
3279
+ "document.read",
3280
+ "document.write",
3281
+ "assets.read",
3282
+ "assets.pick",
3283
+ "ai.suggest",
3284
+ "tools.image.generate",
3285
+ "preview",
3286
+ "package"
3287
+ ]
3288
+ },
3289
+ minItems: 2,
3290
+ uniqueItems: true
3291
+ },
3292
+ purpose: {
3293
+ type: "object",
3294
+ additionalProperties: false,
3295
+ properties: {
3296
+ ko: {
3297
+ type: "string",
3298
+ minLength: 1,
3299
+ maxLength: 200
3300
+ },
3301
+ en: {
3302
+ type: "string",
3303
+ minLength: 1,
3304
+ maxLength: 200
3305
+ }
3306
+ },
3307
+ required: [
3308
+ "ko"
3309
+ ]
3310
+ }
3311
+ },
3312
+ required: [
3313
+ "bundle",
3314
+ "sha256",
3315
+ "permissions",
3316
+ "purpose"
3317
+ ]
2627
3318
  }
2628
3319
  },
2629
3320
  required: [
@@ -2885,7 +3576,8 @@ var clack_skill_v1_schema_default = {
2885
3576
  "fixed",
2886
3577
  "scroll_hide",
2887
3578
  "translucent_scroll_hide",
2888
- "floating_close"
3579
+ "floating_close",
3580
+ "floating_close_safe_area"
2889
3581
  ]
2890
3582
  },
2891
3583
  color_scheme: {
@@ -2900,6 +3592,89 @@ var clack_skill_v1_schema_default = {
2900
3592
  "header_mode",
2901
3593
  "color_scheme"
2902
3594
  ]
3595
+ },
3596
+ data: {
3597
+ type: "object",
3598
+ additionalProperties: false,
3599
+ description: "\uC2A4\uD0AC \uC5D0\uB514\uD130\uAC00 \uB9CC\uB4DC\uB294 \uB370\uC774\uD130 \uBB38\uC11C \uC120\uC5B8. schema \uD30C\uC77C\uC740 output-data.v1 \uADDC\uCE59\uC744 \uB530\uB978\uB2E4. \uBC88\uB4E4\uC5D0\uB294 \uACF5\uAC1C \uBD80\uBD84\uC774 data/document.json, \uBE44\uACF5\uAC1C \uCD5C\uC0C1\uC704 \uD0A4\uAC00 .clack/config.private.json, \uC790\uC0B0\uC774 data/assets/\uB85C \uB4E4\uC5B4\uAC04\uB2E4.",
3600
+ properties: {
3601
+ schema: {
3602
+ type: "string",
3603
+ pattern: "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))[A-Za-z0-9_./-]+$"
3604
+ },
3605
+ max_bytes: {
3606
+ type: "integer",
3607
+ minimum: 1024,
3608
+ maximum: 262144
3609
+ },
3610
+ private_max_bytes: {
3611
+ type: "integer",
3612
+ minimum: 1,
3613
+ maximum: 65536
3614
+ },
3615
+ assets: {
3616
+ type: "object",
3617
+ additionalProperties: false,
3618
+ properties: {
3619
+ max_count: {
3620
+ type: "integer",
3621
+ minimum: 1,
3622
+ maximum: 48
3623
+ },
3624
+ max_total_bytes: {
3625
+ type: "integer",
3626
+ minimum: 1,
3627
+ maximum: 25165824
3628
+ }
3629
+ },
3630
+ required: [
3631
+ "max_count",
3632
+ "max_total_bytes"
3633
+ ]
3634
+ }
3635
+ },
3636
+ required: [
3637
+ "schema"
3638
+ ]
3639
+ },
3640
+ intro: {
3641
+ type: "object",
3642
+ additionalProperties: false,
3643
+ description: "\uCF58\uD150\uCE20 \uC778\uD2B8\uB85C(\uC2E4\uD589 \uC804 \uAE30\uBCF8 \uC815\uBCF4 \uD654\uBA74) \uCD9C\uB825 \uC120\uC5B8. \uBAA8\uB4E0 \uD0A4\uB294 \uC120\uD0DD\uC774\uB2E4. creator_comment\xB7description\uC740 \uACF5\uAC1C \uBB38\uC790\uC5F4 \uD3FC \uD544\uB4DC(\uCD5C\uB300 \uAE38\uC774 500\xB75000 \uC774\uD558), description_fallback\uC740 description \uAC12\uC774 \uBE44\uC5C8\uC744 \uB54C \uC21C\uC11C\uB300\uB85C \uAC12\uC774 \uC788\uB294 \uD544\uB4DC\uB9CC '{\uD3FC \uD544\uB4DC title}\\n{\uAC12}'\uC744 \uBE48 \uC904\uB85C \uC774\uC5B4 \uC0C1\uC138 \uC124\uBA85\uC744 \uC790\uB3D9 \uAD6C\uC131\uD560 \uBB38\uC790\uC5F4 \uD3FC \uD544\uB4DC\uB2E4(\uBE44\uACF5\uAC1C \uD544\uB4DC\uB97C \uACF5\uAC1C\uD558\uB294 \uC720\uC77C\uD55C \uBA85\uC2DC \uC608\uC678, \uCD5C\uB300 \uAE38\uC774 \uD569\uACFC \uB77C\uBCA8\uC774 5000\uC790 \uC774\uD558). images\uB294 output.asset_slots\uC758 field\uC774\uBA70 \uC2AC\uB86F \uC21C\uC11C\uB300\uB85C \uD3BC\uCE5C \uC81C\uC791 \uC790\uC0B0\uC744 \uC4F4\uB2E4(\uB2E8\uC77C 1\uC7A5\xB7multiple\uC740 max\uC7A5\uC73C\uB85C \uC138\uC5B4 \uD569\uACC4 8 \uC774\uD558). \uD3FC \uC2A4\uD0AC \uC804\uC6A9\uC774\uBA70 \uC5D0\uB514\uD130 \uC2A4\uD0AC\xB7\uC9C0\uCE68\uD615(instruction) \uC2A4\uD0AC\uC740 \uC4F8 \uC218 \uC5C6\uB2E4.",
3644
+ properties: {
3645
+ default_enabled: {
3646
+ type: "boolean",
3647
+ default: false
3648
+ },
3649
+ creator_comment: {
3650
+ type: "string",
3651
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3652
+ },
3653
+ description: {
3654
+ type: "string",
3655
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3656
+ },
3657
+ description_fallback: {
3658
+ type: "array",
3659
+ items: {
3660
+ type: "string",
3661
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3662
+ },
3663
+ minItems: 1,
3664
+ maxItems: 20,
3665
+ uniqueItems: true
3666
+ },
3667
+ images: {
3668
+ type: "array",
3669
+ items: {
3670
+ type: "string",
3671
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3672
+ },
3673
+ minItems: 1,
3674
+ maxItems: 8,
3675
+ uniqueItems: true
3676
+ }
3677
+ }
2903
3678
  }
2904
3679
  },
2905
3680
  required: [
@@ -3042,8 +3817,27 @@ var clack_skill_v1_schema_default = {
3042
3817
  ],
3043
3818
  properties: {
3044
3819
  authoring: {
3045
- required: [
3046
- "form"
3820
+ oneOf: [
3821
+ {
3822
+ required: [
3823
+ "form"
3824
+ ],
3825
+ not: {
3826
+ required: [
3827
+ "editor"
3828
+ ]
3829
+ }
3830
+ },
3831
+ {
3832
+ required: [
3833
+ "editor"
3834
+ ],
3835
+ not: {
3836
+ required: [
3837
+ "form"
3838
+ ]
3839
+ }
3840
+ }
3047
3841
  ],
3048
3842
  properties: {
3049
3843
  modes: {
@@ -3094,6 +3888,136 @@ var clack_skill_v1_schema_default = {
3094
3888
  "runtime"
3095
3889
  ]
3096
3890
  }
3891
+ },
3892
+ {
3893
+ if: {
3894
+ properties: {
3895
+ authoring: {
3896
+ required: [
3897
+ "editor"
3898
+ ]
3899
+ }
3900
+ },
3901
+ required: [
3902
+ "authoring"
3903
+ ]
3904
+ },
3905
+ then: {
3906
+ properties: {
3907
+ type: {
3908
+ const: "template"
3909
+ },
3910
+ authoring: {
3911
+ properties: {
3912
+ surfaces: {
3913
+ items: {
3914
+ const: "center"
3915
+ },
3916
+ maxItems: 1
3917
+ }
3918
+ },
3919
+ not: {
3920
+ anyOf: [
3921
+ {
3922
+ required: [
3923
+ "form"
3924
+ ]
3925
+ },
3926
+ {
3927
+ required: [
3928
+ "plugin"
3929
+ ]
3930
+ }
3931
+ ]
3932
+ }
3933
+ },
3934
+ output: {
3935
+ required: [
3936
+ "data"
3937
+ ],
3938
+ not: {
3939
+ anyOf: [
3940
+ {
3941
+ required: [
3942
+ "asset_slots"
3943
+ ]
3944
+ },
3945
+ {
3946
+ required: [
3947
+ "derive"
3948
+ ]
3949
+ },
3950
+ {
3951
+ required: [
3952
+ "profile_select"
3953
+ ]
3954
+ },
3955
+ {
3956
+ required: [
3957
+ "entity"
3958
+ ]
3959
+ },
3960
+ {
3961
+ required: [
3962
+ "entity_map"
3963
+ ]
3964
+ },
3965
+ {
3966
+ required: [
3967
+ "intro"
3968
+ ]
3969
+ }
3970
+ ]
3971
+ }
3972
+ }
3973
+ }
3974
+ }
3975
+ },
3976
+ {
3977
+ if: {
3978
+ properties: {
3979
+ output: {
3980
+ required: [
3981
+ "data"
3982
+ ]
3983
+ }
3984
+ },
3985
+ required: [
3986
+ "output"
3987
+ ]
3988
+ },
3989
+ then: {
3990
+ properties: {
3991
+ authoring: {
3992
+ required: [
3993
+ "editor"
3994
+ ]
3995
+ }
3996
+ }
3997
+ }
3998
+ },
3999
+ {
4000
+ if: {
4001
+ properties: {
4002
+ type: {
4003
+ const: "instruction"
4004
+ }
4005
+ },
4006
+ required: [
4007
+ "type"
4008
+ ]
4009
+ },
4010
+ then: {
4011
+ properties: {
4012
+ output: {
4013
+ not: {
4014
+ required: [
4015
+ "intro"
4016
+ ]
4017
+ }
4018
+ }
4019
+ }
4020
+ }
3097
4021
  }
3098
4022
  ]
3099
4023
  };
@@ -3977,39 +4901,39 @@ var clack_content_v1_schema_default = {
3977
4901
  var MAX_ZIP = 10 * 1024 * 1024;
3978
4902
  var MAX_EXPANDED = 30 * 1024 * 1024;
3979
4903
  var MAX_FILES2 = 500;
3980
- var textDecoder = new TextDecoder("utf-8", { fatal: true });
3981
- var ajv = new Ajv2020({ allErrors: true, strict: false });
4904
+ var textDecoder2 = new TextDecoder("utf-8", { fatal: true });
4905
+ var ajv = new Ajv20202({ allErrors: true, strict: false });
3982
4906
  ajv.addSchema(clack_content_v1_schema_default);
3983
4907
  var validateManifest = ajv.compile(clack_skill_v1_schema_default);
3984
- var validateForm = new Ajv2020({ allErrors: true, strict: false }).compile(authoring_form_v1_schema_default);
3985
- function invalid(message) {
4908
+ var validateForm = new Ajv20202({ allErrors: true, strict: false }).compile(authoring_form_v1_schema_default);
4909
+ function invalid2(message) {
3986
4910
  throw new CliError(message, "SKILL_PACKAGE_INVALID");
3987
4911
  }
3988
- function hash(bytes) {
3989
- return createHash3("sha256").update(bytes).digest("hex");
4912
+ function hash(bytes2) {
4913
+ return createHash4("sha256").update(bytes2).digest("hex");
3990
4914
  }
3991
4915
  function object(value2) {
3992
4916
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
3993
4917
  }
3994
- function json(bytes, max, label) {
3995
- if (bytes.length > max) invalid(`${label} \uD06C\uAE30 \uC81C\uD55C\uC744 \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.`);
4918
+ function json(bytes2, max, label) {
4919
+ if (bytes2.length > max) invalid2(`${label} \uD06C\uAE30 \uC81C\uD55C\uC744 \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.`);
3996
4920
  try {
3997
- const parsed = JSON.parse(textDecoder.decode(bytes));
4921
+ const parsed = JSON.parse(textDecoder2.decode(bytes2));
3998
4922
  if (object(parsed)) return parsed;
3999
4923
  } catch {
4000
4924
  }
4001
- return invalid(`${label}\uC740 UTF-8 JSON \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
4925
+ return invalid2(`${label}\uC740 UTF-8 JSON \uAC1D\uCCB4\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
4002
4926
  }
4003
4927
  async function readRegular(path) {
4004
4928
  let handle;
4005
4929
  try {
4006
4930
  handle = await open3(path, constants.O_RDONLY | constants.O_NOFOLLOW);
4007
4931
  const info = await handle.stat();
4008
- if (!info.isFile()) invalid("\uC2A4\uD0AC \uD328\uD0A4\uC9C0\uC5D0\uB294 \uC77C\uBC18 \uD30C\uC77C\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4.");
4932
+ if (!info.isFile()) invalid2("\uC2A4\uD0AC \uD328\uD0A4\uC9C0\uC5D0\uB294 \uC77C\uBC18 \uD30C\uC77C\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4.");
4009
4933
  return await handle.readFile();
4010
4934
  } catch (error2) {
4011
4935
  if (error2 instanceof CliError) throw error2;
4012
- return invalid("\uC2A4\uD0AC \uD30C\uC77C\uC744 \uC548\uC804\uD558\uAC8C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
4936
+ return invalid2("\uC2A4\uD0AC \uD30C\uC77C\uC744 \uC548\uC804\uD558\uAC8C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
4013
4937
  } finally {
4014
4938
  await handle?.close();
4015
4939
  }
@@ -4028,76 +4952,76 @@ async function collectFiles(directory) {
4028
4952
  const entries = await readdir(join2(directory, relative), { withFileTypes: true });
4029
4953
  for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
4030
4954
  const path = relative ? `${relative}/${entry.name}` : entry.name;
4031
- if (!skillPathAllowed(path)) invalid(`\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD328\uD0A4\uC9C0 \uACBD\uB85C: ${path}`);
4032
- if (entry.isSymbolicLink() || !(entry.isDirectory() || entry.isFile())) invalid(`\uC77C\uBC18 \uD30C\uC77C\xB7\uD3F4\uB354\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4: ${path}`);
4955
+ if (!skillPathAllowed(path)) invalid2(`\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD328\uD0A4\uC9C0 \uACBD\uB85C: ${path}`);
4956
+ if (entry.isSymbolicLink() || !(entry.isDirectory() || entry.isFile())) invalid2(`\uC77C\uBC18 \uD30C\uC77C\xB7\uD3F4\uB354\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4: ${path}`);
4033
4957
  if (entry.isDirectory()) {
4034
4958
  await walk(path);
4035
4959
  continue;
4036
4960
  }
4037
- if (Object.keys(files).length >= MAX_FILES2) invalid("\uC2A4\uD0AC \uD30C\uC77C \uC218\uAC00 500\uAC1C\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
4961
+ if (Object.keys(files).length >= MAX_FILES2) invalid2("\uC2A4\uD0AC \uD30C\uC77C \uC218\uAC00 500\uAC1C\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
4038
4962
  const data = await readRegular(join2(directory, path));
4039
4963
  totalBytes += data.length;
4040
- if (totalBytes > MAX_EXPANDED) invalid("\uC2A4\uD0AC \uC555\uCD95 \uD574\uC81C \uC6A9\uB7C9\uC774 30 MiB\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
4964
+ if (totalBytes > MAX_EXPANDED) invalid2("\uC2A4\uD0AC \uC555\uCD95 \uD574\uC81C \uC6A9\uB7C9\uC774 30 MiB\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
4041
4965
  files[path] = data;
4042
4966
  }
4043
4967
  }
4044
4968
  await walk("");
4045
4969
  return files;
4046
4970
  }
4047
- function inspectZip(bytes, expectedDirectory) {
4048
- if (!bytes.length || bytes.length > MAX_ZIP) invalid("\uC2A4\uD0AC ZIP\uC740 \uBE44\uC5B4 \uC788\uC9C0 \uC54A\uC740 10 MiB \uC774\uD558 \uD30C\uC77C\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.");
4971
+ function inspectZip(bytes2, expectedDirectory, hashMismatch) {
4972
+ if (!bytes2.length || bytes2.length > MAX_ZIP) invalid2("\uC2A4\uD0AC ZIP\uC740 \uBE44\uC5B4 \uC788\uC9C0 \uC54A\uC740 10 MiB \uC774\uD558 \uD30C\uC77C\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.");
4049
4973
  const seen = /* @__PURE__ */ new Set();
4050
4974
  let entries = 0;
4051
4975
  let totalBytes = 0;
4052
4976
  let extracted;
4053
4977
  try {
4054
- extracted = unzipSync2(bytes, { filter(entry) {
4978
+ extracted = unzipSync2(bytes2, { filter(entry) {
4055
4979
  entries++;
4056
- if (entries > MAX_FILES2) invalid("\uC2A4\uD0AC \uD30C\uC77C \uC218\uAC00 500\uAC1C\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
4980
+ if (entries > MAX_FILES2) invalid2("\uC2A4\uD0AC \uD30C\uC77C \uC218\uAC00 500\uAC1C\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
4057
4981
  const path = entry.name.endsWith("/") ? entry.name.slice(0, -1) : entry.name;
4058
- if (!skillPathAllowed(path) || seen.has(path.toLowerCase())) invalid(`\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 ZIP \uACBD\uB85C: ${path}`);
4982
+ if (!skillPathAllowed(path) || seen.has(path.toLowerCase())) invalid2(`\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 ZIP \uACBD\uB85C: ${path}`);
4059
4983
  seen.add(path.toLowerCase());
4060
4984
  if (entry.name.endsWith("/")) return false;
4061
4985
  totalBytes += entry.originalSize;
4062
- if (totalBytes > MAX_EXPANDED) invalid("\uC2A4\uD0AC \uC555\uCD95 \uD574\uC81C \uC6A9\uB7C9\uC774 30 MiB\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
4986
+ if (totalBytes > MAX_EXPANDED) invalid2("\uC2A4\uD0AC \uC555\uCD95 \uD574\uC81C \uC6A9\uB7C9\uC774 30 MiB\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
4063
4987
  return true;
4064
4988
  } });
4065
4989
  } catch (error2) {
4066
4990
  if (error2 instanceof CliError) throw error2;
4067
- return invalid("ZIP \uAD6C\uC870\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
4991
+ return invalid2("ZIP \uAD6C\uC870\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
4068
4992
  }
4069
4993
  const markdown = extracted["SKILL.md"];
4070
4994
  const manifestBytes = extracted["clack.skill.json"];
4071
- if (!markdown || !manifestBytes) invalid("\uB8E8\uD2B8 SKILL.md\uC640 clack.skill.json\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
4072
- if (markdown.length > 20 * 1024) invalid("SKILL.md\uB294 20 KiB \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
4995
+ if (!markdown || !manifestBytes) invalid2("\uB8E8\uD2B8 SKILL.md\uC640 clack.skill.json\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
4996
+ if (markdown.length > 20 * 1024) invalid2("SKILL.md\uB294 20 KiB \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
4073
4997
  let frontmatter;
4074
4998
  try {
4075
- const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(textDecoder.decode(markdown));
4076
- if (!match) invalid("SKILL.md\uC758 YAML frontmatter\uB97C \uD655\uC778\uD558\uC138\uC694.");
4999
+ const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(textDecoder2.decode(markdown));
5000
+ if (!match) invalid2("SKILL.md\uC758 YAML frontmatter\uB97C \uD655\uC778\uD558\uC138\uC694.");
4077
5001
  frontmatter = match[1];
4078
5002
  } catch {
4079
- return invalid("SKILL.md\uB294 UTF-8 \uD14D\uC2A4\uD2B8\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
5003
+ return invalid2("SKILL.md\uB294 UTF-8 \uD14D\uC2A4\uD2B8\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
4080
5004
  }
4081
5005
  const document = parseDocument(frontmatter, { version: "1.2", uniqueKeys: true, merge: false });
4082
5006
  if (document.errors.length || document.warnings.length || /(?:^|\n)\s*allowed-tools\s*:/i.test(frontmatter)) {
4083
- invalid("SKILL.md frontmatter\uC5D0 \uC798\uBABB\uB41C \uAC12 \uB610\uB294 allowed-tools\uAC00 \uC788\uC2B5\uB2C8\uB2E4.");
5007
+ invalid2("SKILL.md frontmatter\uC5D0 \uC798\uBABB\uB41C \uAC12 \uB610\uB294 allowed-tools\uAC00 \uC788\uC2B5\uB2C8\uB2E4.");
4084
5008
  }
4085
5009
  let front;
4086
5010
  try {
4087
5011
  front = document.toJS({ maxAliasCount: 0 });
4088
5012
  } catch {
4089
- return invalid("SKILL.md frontmatter\uB97C \uD655\uC778\uD558\uC138\uC694.");
5013
+ return invalid2("SKILL.md frontmatter\uB97C \uD655\uC778\uD558\uC138\uC694.");
4090
5014
  }
4091
- if (!object(front) || typeof front.name !== "string" || typeof front.description !== "string" || !front.description.length || front.description.length > 1024) invalid("SKILL.md\uC758 name\xB7description\uC744 \uD655\uC778\uD558\uC138\uC694.");
5015
+ if (!object(front) || typeof front.name !== "string" || typeof front.description !== "string" || !front.description.length || front.description.length > 1024) invalid2("SKILL.md\uC758 name\xB7description\uC744 \uD655\uC778\uD558\uC138\uC694.");
4092
5016
  const manifest = json(manifestBytes, 64 * 1024, "clack.skill.json");
4093
- if (!validateManifest(manifest)) invalid("clack.skill.json\uC774 \uACF5\uAC1C \uC2A4\uD0AC \uC2A4\uD0A4\uB9C8\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
5017
+ if (!validateManifest(manifest)) invalid2("clack.skill.json\uC774 \uACF5\uAC1C \uC2A4\uD0AC \uC2A4\uD0A4\uB9C8\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
4094
5018
  if (manifest.name !== front.name || expectedDirectory && manifest.name !== expectedDirectory && manifest.version !== expectedDirectory || object(front.metadata) && front.metadata.version !== void 0 && front.metadata.version !== manifest.version) {
4095
- invalid("\uC2A4\uD0AC \uB514\uB809\uD130\uB9AC\xB7SKILL.md\xB7\uB9E4\uB2C8\uD398\uC2A4\uD2B8\uC758 \uC774\uB984 \uB610\uB294 \uBC84\uC804\uC774 \uB2E4\uB985\uB2C8\uB2E4.");
5019
+ invalid2("\uC2A4\uD0AC \uB514\uB809\uD130\uB9AC\xB7SKILL.md\xB7\uB9E4\uB2C8\uD398\uC2A4\uD2B8\uC758 \uC774\uB984 \uB610\uB294 \uBC84\uC804\uC774 \uB2E4\uB985\uB2C8\uB2E4.");
4096
5020
  }
4097
5021
  const file = (path, max) => {
4098
- if (typeof path !== "string" || !skillPathAllowed(path) || !extracted[path]) invalid("\uB9E4\uB2C8\uD398\uC2A4\uD2B8\uC5D0 \uC120\uC5B8\uD55C \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
5022
+ if (typeof path !== "string" || !skillPathAllowed(path) || !extracted[path]) invalid2("\uB9E4\uB2C8\uD398\uC2A4\uD2B8\uC5D0 \uC120\uC5B8\uD55C \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
4099
5023
  const data = extracted[path];
4100
- if (max && data.length > max) invalid("\uC120\uC5B8\uD55C \uD30C\uC77C\uC758 \uD06C\uAE30 \uC81C\uD55C\uC744 \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
5024
+ if (max && data.length > max) invalid2("\uC120\uC5B8\uD55C \uD30C\uC77C\uC758 \uD06C\uAE30 \uC81C\uD55C\uC744 \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
4101
5025
  return data;
4102
5026
  };
4103
5027
  const display = manifest.display;
@@ -4106,50 +5030,93 @@ function inspectZip(bytes, expectedDirectory) {
4106
5030
  const authoring = manifest.authoring;
4107
5031
  const output2 = manifest.output;
4108
5032
  for (const reference of authoring.references ?? []) file(reference, 64 * 1024);
4109
- if (manifest.type === "template" || manifest.type === "tool") {
5033
+ let editor;
5034
+ if (manifest.type === "template" && authoring.editor) {
5035
+ editor = inspectEditorSkill(manifest, extracted, { hashMismatch });
5036
+ file(`runtime/${output2.template.entry}`);
5037
+ const runtime2 = manifest.runtime;
5038
+ for (const profile of runtime2.profiles) file(`runtime/.clack/profiles/${profile}.yaml`, 16 * 1024);
5039
+ } else if (manifest.type === "template" || manifest.type === "tool") {
4110
5040
  const form = json(file(authoring.form, 64 * 1024), 64 * 1024, "\uC81C\uC791 \uD3FC");
4111
- if (!validateForm(form)) invalid("\uC81C\uC791 \uD3FC\uC774 \uACF5\uAC1C \uC2A4\uD0A4\uB9C8\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
5041
+ if (!validateForm(form)) invalid2("\uC81C\uC791 \uD3FC\uC774 \uACF5\uAC1C \uC2A4\uD0A4\uB9C8\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
4112
5042
  const semanticErrors = inspectAuthoringSemantics(manifest, form);
4113
- if (semanticErrors.length) invalid(semanticErrors.map((error2) => `${error2.path}: ${error2.message}`).join(" / "));
5043
+ if (semanticErrors.length) invalid2(semanticErrors.map((error2) => `${error2.path}: ${error2.message}`).join(" / "));
4114
5044
  const template = output2.template;
4115
- if (template.root !== "runtime" || typeof template.entry !== "string") invalid("runtime \uD15C\uD50C\uB9BF \uACBD\uB85C\uB97C \uD655\uC778\uD558\uC138\uC694.");
5045
+ if (template.root !== "runtime" || typeof template.entry !== "string") invalid2("runtime \uD15C\uD50C\uB9BF \uACBD\uB85C\uB97C \uD655\uC778\uD558\uC138\uC694.");
4116
5046
  file(`runtime/${template.entry}`);
4117
5047
  const runtime2 = manifest.runtime;
4118
5048
  for (const profile of runtime2.profiles) file(`runtime/.clack/profiles/${profile}.yaml`, 16 * 1024);
4119
5049
  const examples = Object.keys(extracted).filter((path) => /^examples\/[^/]+\.json$/.test(path));
4120
- if (!examples.length || examples.length > 10) invalid("\uD15C\uD50C\uB9BF \uC608\uC2DC\uB294 1~10\uAC1C \uD544\uC694\uD569\uB2C8\uB2E4.");
5050
+ if (!examples.length || examples.length > 10) invalid2("\uD15C\uD50C\uB9BF \uC608\uC2DC\uB294 1~10\uAC1C \uD544\uC694\uD569\uB2C8\uB2E4.");
4121
5051
  let validateExample;
4122
5052
  try {
4123
- const examplesAjv = new Ajv2020({ allErrors: true, strict: false });
5053
+ const examplesAjv = new Ajv20202({ allErrors: true, strict: false });
4124
5054
  examplesAjv.addFormat("uuid", /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
4125
5055
  validateExample = examplesAjv.compile(form);
4126
5056
  } catch {
4127
- return invalid("\uC81C\uC791 \uD3FC JSON Schema\uB97C \uD655\uC778\uD558\uC138\uC694.");
5057
+ return invalid2("\uC81C\uC791 \uD3FC JSON Schema\uB97C \uD655\uC778\uD558\uC138\uC694.");
4128
5058
  }
4129
5059
  for (const path of examples) if (!validateExample(json(file(path, 64 * 1024), 64 * 1024, "\uC608\uC2DC"))) {
4130
- invalid("\uC81C\uC791 \uD3FC\uACFC \uC608\uC2DC \uB370\uC774\uD130\uAC00 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
5060
+ invalid2("\uC81C\uC791 \uD3FC\uACFC \uC608\uC2DC \uB370\uC774\uD130\uAC00 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
4131
5061
  }
4132
5062
  }
4133
- return { manifest, file_count: Object.keys(extracted).length, total_bytes: totalBytes };
5063
+ return {
5064
+ manifest,
5065
+ file_count: Object.keys(extracted).length,
5066
+ total_bytes: totalBytes,
5067
+ ...editor ? { editor: editor.summary, warnings: editor.warnings } : {}
5068
+ };
5069
+ }
5070
+ function fillEditorHash(files) {
5071
+ const manifestBytes = files["clack.skill.json"];
5072
+ if (!manifestBytes) return null;
5073
+ let text;
5074
+ let manifest;
5075
+ try {
5076
+ text = textDecoder2.decode(manifestBytes);
5077
+ manifest = JSON.parse(text);
5078
+ } catch {
5079
+ return null;
5080
+ }
5081
+ if (!object(manifest)) return null;
5082
+ const computed = computeEditorHash(manifest, files);
5083
+ const editor = object(manifest.authoring) ? manifest.authoring.editor : void 0;
5084
+ if (!computed || !object(editor) || typeof editor.sha256 !== "string" || editor.sha256 === computed) return null;
5085
+ const declared = editor.sha256;
5086
+ const next = text.split(declared).length === 2 ? text.replace(declared, computed) : `${JSON.stringify({ ...manifest, authoring: { ...manifest.authoring, editor: { ...editor, sha256: computed } } }, null, 2)}
5087
+ `;
5088
+ return new TextEncoder().encode(next);
4134
5089
  }
4135
- async function prepareSkillPackage(source) {
5090
+ async function prepareSkillPackage(source, options2 = {}) {
4136
5091
  const path = resolve7(source);
4137
5092
  let info;
4138
5093
  try {
4139
5094
  info = await lstat4(path);
4140
5095
  } catch {
4141
- return invalid("\uC2A4\uD0AC \uB514\uB809\uD130\uB9AC \uB610\uB294 ZIP \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
5096
+ return invalid2("\uC2A4\uD0AC \uB514\uB809\uD130\uB9AC \uB610\uB294 ZIP \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
4142
5097
  }
4143
- if (info.isSymbolicLink()) invalid("\uC2A4\uD0AC \uACBD\uB85C\uB294 \uC2EC\uBCFC\uB9AD \uB9C1\uD06C\uC77C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
4144
- let bytes;
5098
+ if (info.isSymbolicLink()) invalid2("\uC2A4\uD0AC \uACBD\uB85C\uB294 \uC2EC\uBCFC\uB9AD \uB9C1\uD06C\uC77C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
5099
+ let bytes2;
5100
+ let filledManifest;
4145
5101
  if (info.isDirectory()) {
4146
5102
  const files = await collectFiles(path);
4147
- if (!Object.keys(files).length) invalid("\uC2A4\uD0AC \uB514\uB809\uD130\uB9AC\uAC00 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.");
4148
- bytes = Buffer.from(zipSync(files, { level: 6, mtime: new Date(2e3, 0, 1) }));
4149
- } else if (info.isFile() && extname2(path).toLowerCase() === ".zip") bytes = await readRegular(path);
4150
- else return invalid("\uC2A4\uD0AC \uB514\uB809\uD130\uB9AC \uB610\uB294 ZIP \uD30C\uC77C\uC744 \uC9C0\uC815\uD558\uC138\uC694.");
4151
- const inspected = inspectZip(bytes, info.isDirectory() ? basename4(path) : void 0);
4152
- return { bytes, sha256: hash(bytes), ...inspected };
5103
+ if (!Object.keys(files).length) invalid2("\uC2A4\uD0AC \uB514\uB809\uD130\uB9AC\uAC00 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.");
5104
+ if (options2.fillEditorHash) {
5105
+ const filled = fillEditorHash(files);
5106
+ if (filled) {
5107
+ files["clack.skill.json"] = filled;
5108
+ filledManifest = filled;
5109
+ }
5110
+ }
5111
+ bytes2 = Buffer.from(zipSync(files, { level: 6, mtime: new Date(2e3, 0, 1) }));
5112
+ } else if (info.isFile() && extname2(path).toLowerCase() === ".zip") bytes2 = await readRegular(path);
5113
+ else return invalid2("\uC2A4\uD0AC \uB514\uB809\uD130\uB9AC \uB610\uB294 ZIP \uD30C\uC77C\uC744 \uC9C0\uC815\uD558\uC138\uC694.");
5114
+ const inspected = inspectZip(
5115
+ bytes2,
5116
+ info.isDirectory() ? basename4(path) : void 0,
5117
+ info.isDirectory() && !options2.fillEditorHash ? "warn" : "error"
5118
+ );
5119
+ return { bytes: bytes2, sha256: hash(bytes2), ...inspected, ...filledManifest ? { filled_manifest: filledManifest } : {} };
4153
5120
  }
4154
5121
 
4155
5122
  // src/commands/skill.ts
@@ -4192,7 +5159,7 @@ function ensurePat(client, message = PAT_FOR_WRITE) {
4192
5159
  }
4193
5160
  return token;
4194
5161
  }
4195
- async function validateSkillRemotely(client, bytes) {
5162
+ async function validateSkillRemotely(client, bytes2) {
4196
5163
  const token = ensurePat(client);
4197
5164
  const url = new URL("/v4/creator/skills/validate", client.settings.baseUrl);
4198
5165
  let response;
@@ -4210,7 +5177,7 @@ async function validateSkillRemotely(client, bytes) {
4210
5177
  "X-CLACK-Time-Contract": "utc-v1",
4211
5178
  "Cache-Control": "no-store"
4212
5179
  },
4213
- body: new Uint8Array(bytes),
5180
+ body: new Uint8Array(bytes2),
4214
5181
  redirect: "error",
4215
5182
  signal: AbortSignal.timeout(3e4)
4216
5183
  });
@@ -4296,13 +5263,13 @@ function registerSkillCommands(program2, runtime2) {
4296
5263
  }, contract });
4297
5264
  if (!Array.isArray(result.data)) throw new CliError("\uC2A4\uD0AC \uBAA9\uB85D \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.", "INVALID_RESPONSE", 502);
4298
5265
  if (!opts.all) {
4299
- ctx.output(result);
5266
+ ctx.output({ ...result, data: markCenterOnly(result.data) });
4300
5267
  return;
4301
5268
  }
4302
5269
  contract = result.time_contract;
4303
5270
  all.push(...result.data);
4304
5271
  if (!result.pagination?.has_more) {
4305
- ctx.output({ data: all, time_contract: result.time_contract });
5272
+ ctx.output({ data: markCenterOnly(all), time_contract: result.time_contract });
4306
5273
  return;
4307
5274
  }
4308
5275
  const next = result.pagination.next_cursor;
@@ -4316,7 +5283,8 @@ function registerSkillCommands(program2, runtime2) {
4316
5283
  runtime2.action(skill.command("get <slug>").description("\uC2A4\uD0AC \uC0C1\uC138 \uC870\uD68C (skill:read)"), async (ctx, [value2]) => {
4317
5284
  const parsed = skillSlug.safeParse(value2);
4318
5285
  if (!parsed.success) throw new CliError("\uC2A4\uD0AC \uC774\uB984\uC744 \uD655\uC778\uD558\uC138\uC694.");
4319
- ctx.output(await ctx.api.request("GET", `/v4/skills/${parsed.data}`));
5286
+ const detail = await ctx.api.request("GET", `/v4/skills/${parsed.data}`);
5287
+ ctx.output({ ...detail, data: markCenterOnly(detail.data) });
4320
5288
  });
4321
5289
  runtime2.action(skill.command("form <slug> <version>").description("\uC2A4\uD0AC \uBC84\uC804 \uC785\uB825 \uC591\uC2DD \uC870\uD68C (skill:read)"), async (ctx, [value2, version]) => {
4322
5290
  const parsed = skillSlug.safeParse(value2);
@@ -4330,27 +5298,73 @@ function registerSkillCommands(program2, runtime2) {
4330
5298
  });
4331
5299
  runtime2.action(skill.command("validate [dir]").description("\uC2A4\uD0AC \uD328\uD0A4\uC9C0 \uB85C\uCEEC \uAC80\uC0AC (\uB85C\uADF8\uC778 \uBD88\uD544\uC694, --remote\uB294 skill:write)").option("--remote", "\uC11C\uBC84\uC5D0\uC11C\uB3C4 \uD328\uD0A4\uC9C0\uB97C \uB4DC\uB77C\uC774\uB7F0 \uAC80\uC99D (skill:write)"), async (ctx, [dir], opts) => {
4332
5300
  const prepared = await prepareSkillPackage(dir ?? ".");
4333
- if (opts.remote && !ctx.options.dryRun) {
4334
- const remote = await validateSkillRemotely(ctx.api, prepared.bytes);
4335
- ctx.output({
4336
- local_valid: true,
4337
- sha256: prepared.sha256,
4338
- byte_size: prepared.bytes.length,
4339
- file_count: prepared.file_count,
4340
- manifest: prepared.manifest,
4341
- remote
4342
- });
4343
- } else ctx.output({
5301
+ const local = {
4344
5302
  local_valid: true,
4345
5303
  sha256: prepared.sha256,
4346
5304
  byte_size: prepared.bytes.length,
4347
5305
  file_count: prepared.file_count,
4348
5306
  manifest: prepared.manifest,
4349
- ...opts.remote ? { remote_skipped: true } : {}
5307
+ ...editorReport(prepared)
5308
+ };
5309
+ if (opts.remote && !ctx.options.dryRun) {
5310
+ const remote = await validateSkillRemotely(ctx.api, prepared.bytes);
5311
+ ctx.output({ ...local, remote });
5312
+ } else ctx.output({ ...local, ...opts.remote ? { remote_skipped: true } : {} });
5313
+ });
5314
+ runtime2.action(skill.command("pack [dir]").description("\uC2A4\uD0AC \uD328\uD0A4\uC9C0 ZIP \uB9CC\uB4E4\uAE30 (\uB85C\uADF8\uC778 \uBD88\uD544\uC694, \uC5D0\uB514\uD130 \uC2A4\uD0AC\uC740 authoring.editor.sha256\uC744 \uCC44\uC6C0)").option("-o, --out <file>", "ZIP \uC800\uC7A5 \uACBD\uB85C (\uAE30\uBCF8: <\uC774\uB984>-<\uBC84\uC804>.zip)").option("--write-manifest", "\uCC44\uC6B4 sha256\uC744 \uB514\uB809\uD130\uB9AC\uC758 clack.skill.json\uC5D0\uB3C4 \uAE30\uB85D"), async (ctx, [dir], opts) => {
5315
+ const source = dir ?? ".";
5316
+ const prepared = await prepareSkillPackage(source, { fillEditorHash: true });
5317
+ const out = resolve8(opts.out ?? `${prepared.manifest.name}-${prepared.manifest.version}.zip`);
5318
+ const writeManifest = Boolean(opts.writeManifest && prepared.filled_manifest);
5319
+ if (!ctx.options.dryRun) {
5320
+ await writeFile2(out, prepared.bytes);
5321
+ if (writeManifest) await writeFile2(join3(resolve8(source), "clack.skill.json"), prepared.filled_manifest);
5322
+ }
5323
+ ctx.output({
5324
+ ...ctx.options.dryRun ? { dry_run: true } : {},
5325
+ out,
5326
+ sha256: prepared.sha256,
5327
+ byte_size: prepared.bytes.length,
5328
+ file_count: prepared.file_count,
5329
+ slug: prepared.manifest.name,
5330
+ version: prepared.manifest.version,
5331
+ ...prepared.filled_manifest ? { editor_hash_filled: true, manifest_written: writeManifest && !ctx.options.dryRun } : {},
5332
+ ...editorReport(prepared)
4350
5333
  });
4351
5334
  });
5335
+ const editor = skill.command("editor").description("\uC2A4\uD0AC \uC5D0\uB514\uD130(authoring.editor) \uAC1C\uBC1C \uB3C4\uAD6C \u2014 \uC5D0\uB514\uD130 \uC2A4\uD0AC\uC740 \uD06C\uB9AC\uC5D0\uC774\uD130 \uC13C\uD130 \uC804\uC6A9");
5336
+ runtime2.action(
5337
+ editor.command("dev").description("\uB85C\uCEEC \uBAA8\uC758 \uD638\uC2A4\uD2B8\uC5D0\uC11C \uC5D0\uB514\uD130\uB97C \uB744\uC6B4\uB2E4 (http://localhost:5170, \uB85C\uADF8\uC778 \uBD88\uD544\uC694)").option("--editor <dir>", "\uC5D0\uB514\uD130 \uBC88\uB4E4 \uB514\uB809\uD130\uB9AC (\uAE30\uBCF8: SDK \uC2A4\uD0C0\uD130)").option("--document <file>", "\uCD08\uAE30 \uB370\uC774\uD130 \uBB38\uC11C JSON (\uAE30\uBCF8: SDK \uC2A4\uD0C0\uD130 \uC608\uC2DC)").option("--schema <file>", "\uB370\uC774\uD130 \uC2A4\uD0A4\uB9C8 output/data.schema.json (\uAE30\uBCF8: SDK \uC2A4\uD0C0\uD130)").option("--host-port <port>", "\uD638\uC2A4\uD2B8 \uD398\uC774\uC9C0 \uD3EC\uD2B8", "5170").option("--editor-port <port>", "\uC5D0\uB514\uD130 \uD3EC\uD2B8", "5171").option("--sdk-dir <dir>", "@clack/skill-editor-sdk \uC704\uCE58 (\uAE30\uBCF8: CLACK_SKILL_EDITOR_SDK_DIR, \uC124\uCE58\uB41C \uD328\uD0A4\uC9C0, CLACK_MONOREPO_DIR/clack-skill-editor-sdk \uC21C)").addHelpText("after", `
5338
+ ${SDK_LOCATION_HELP}
5339
+ `),
5340
+ async (ctx, _args, opts) => {
5341
+ const args = [];
5342
+ for (const [flag, value2] of [
5343
+ ["editor", opts.editor],
5344
+ ["document", opts.document],
5345
+ ["schema", opts.schema],
5346
+ ["host-port", opts.hostPort],
5347
+ ["editor-port", opts.editorPort]
5348
+ ]) {
5349
+ if (value2 === void 0) continue;
5350
+ if ((flag === "host-port" || flag === "editor-port") && !/^\d{2,5}$/.test(value2)) throw new CliError("\uD3EC\uD2B8\uB294 \uC22B\uC790\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
5351
+ args.push(`--${flag}`, flag.endsWith("port") ? value2 : resolve8(value2));
5352
+ }
5353
+ const sdkDir = resolveSdkDir(opts.sdkDir);
5354
+ const serve = join3(sdkDir, "mock-host/serve.mjs");
5355
+ if (ctx.options.dryRun) {
5356
+ ctx.output({ dry_run: true, sdk_dir: sdkDir, command: ["node", serve, ...args] });
5357
+ return;
5358
+ }
5359
+ await new Promise((done, fail2) => {
5360
+ const child = spawn(process.execPath, [serve, ...args], { stdio: "inherit" });
5361
+ child.on("error", () => fail2(new CliError("\uBAA8\uC758 \uD638\uC2A4\uD2B8\uB97C \uC2E4\uD589\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.", "SKILL_EDITOR_DEV_FAILED")));
5362
+ child.on("exit", (code) => code ? fail2(new CliError(`\uBAA8\uC758 \uD638\uC2A4\uD2B8\uAC00 \uC885\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4(\uCF54\uB4DC ${code}). SDK\uC5D0\uC11C pnpm build\uB97C \uBA3C\uC800 \uC2E4\uD589\uD588\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.`, "SKILL_EDITOR_DEV_FAILED")) : done());
5363
+ });
5364
+ }
5365
+ );
4352
5366
  runtime2.action(skill.command("push [dir]").description("\uC2A4\uD0AC \uD328\uD0A4\uC9C0 \uAC80\uC99D \uD6C4 \uC0C8 \uBC84\uC804 \uC5C5\uB85C\uB4DC (skill:write)").option("--skill-id <id>", "\uAE30\uC874 \uC2A4\uD0AC ID\uC5D0 \uC0C8 \uBC84\uC804 \uCD94\uAC00"), async (ctx, [dir], opts) => {
4353
- const prepared = await prepareSkillPackage(dir ?? ".");
5367
+ const prepared = await prepareSkillPackage(dir ?? ".", { fillEditorHash: true });
4354
5368
  if (prepared.manifest.type !== "instruction" && prepared.manifest.type !== "template") {
4355
5369
  throw new CliError("\uD604\uC7AC \uC2A4\uD0AC \uC5C5\uB85C\uB4DC\uB294 instruction\xB7template \uC720\uD615\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4.", "SKILL_TYPE_UNSUPPORTED");
4356
5370
  }
@@ -4454,6 +5468,39 @@ function registerSkillCommands(program2, runtime2) {
4454
5468
  }
4455
5469
  );
4456
5470
  }
5471
+ function markCenterOnly(value2) {
5472
+ const mark2 = (item) => {
5473
+ if (!item || typeof item !== "object" || Array.isArray(item)) return item;
5474
+ const row = item;
5475
+ return row.center_only === true || row.editor === true ? { ...row, center_only: true, availability: "\uC13C\uD130 \uC804\uC6A9" } : row;
5476
+ };
5477
+ return Array.isArray(value2) ? value2.map(mark2) : mark2(value2);
5478
+ }
5479
+ function editorReport(prepared) {
5480
+ if (!prepared.editor) return {};
5481
+ return {
5482
+ editor: prepared.editor,
5483
+ center_only: true,
5484
+ ...prepared.warnings?.length ? { warnings: prepared.warnings } : {},
5485
+ notice: "\uB85C\uCEEC \uAC80\uC0AC\uB294 \uAD6C\uC870\xB7\uD55C\uB3C4\xB7\uD574\uC2DC\xB7\uB370\uC774\uD130 \uC2A4\uD0A4\uB9C8\xB7\uC608\uC2DC\uAE4C\uC9C0\uC785\uB2C8\uB2E4. \uC5D0\uB514\uD130 \uCF54\uB4DC \uC815\uC801 \uAC80\uC0AC(editor-static-v1)\uC640 \uC2EC\uC0AC\uB294 \uC11C\uBC84 \uD310\uC815\uC774 \uCD5C\uC885\uC785\uB2C8\uB2E4."
5486
+ };
5487
+ }
5488
+ var SDK_LOCATION_HELP = "SDK\uB294 \uC544\uC9C1 npm\uC5D0 \uACF5\uAC1C\uB418\uC9C0 \uC54A\uC544 CLI\uC5D0 \uD3EC\uD568\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. SDK \uC18C\uC2A4 \uB514\uB809\uD130\uB9AC(\uD074\uB799 \uB0B4\uBD80 \uC791\uC5C5\uC790\uB294 \uB0B4\uBD80 \uC800\uC7A5\uC18C\uC758 clack-skill-editor-sdk)\uC5D0\uC11C pnpm install && pnpm build\uB97C \uC2E4\uD589\uD55C \uB4A4 --sdk-dir <SDK \uB514\uB809\uD130\uB9AC>, CLACK_SKILL_EDITOR_SDK_DIR=<SDK \uB514\uB809\uD130\uB9AC>, CLACK_MONOREPO_DIR=<\uB0B4\uBD80 \uC800\uC7A5\uC18C> \uC911 \uD558\uB098\uB85C \uC704\uCE58\uB97C \uC9C0\uC815\uD558\uC138\uC694.";
5489
+ function resolveSdkDir(option) {
5490
+ const candidates = [];
5491
+ if (option) candidates.push(resolve8(option));
5492
+ else {
5493
+ if (process.env.CLACK_SKILL_EDITOR_SDK_DIR) candidates.push(resolve8(process.env.CLACK_SKILL_EDITOR_SDK_DIR));
5494
+ try {
5495
+ candidates.push(resolve8(createRequire(join3(process.cwd(), "noop.js")).resolve("@clack/skill-editor-sdk/package.json"), ".."));
5496
+ } catch {
5497
+ }
5498
+ if (process.env.CLACK_MONOREPO_DIR) candidates.push(resolve8(process.env.CLACK_MONOREPO_DIR, "clack-skill-editor-sdk"));
5499
+ }
5500
+ const found = candidates.find((dir) => existsSync(join3(dir, "mock-host/serve.mjs")));
5501
+ if (!found) throw new CliError(`\uC2A4\uD0AC \uC5D0\uB514\uD130 SDK(@clack/skill-editor-sdk)\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. ${SDK_LOCATION_HELP}`, "SKILL_EDITOR_SDK_NOT_FOUND");
5502
+ return found;
5503
+ }
4457
5504
  function skillVersionIds(skillId, versionId) {
4458
5505
  const parsedSkillId = uuid2.safeParse(skillId);
4459
5506
  const parsedVersionId = uuid2.safeParse(versionId);
@@ -4631,7 +5678,7 @@ var toolDefinitions = [
4631
5678
  function error(id2, code, message) {
4632
5679
  return { jsonrpc: "2.0", id: id2, error: { code, message } };
4633
5680
  }
4634
- function isObject2(value2) {
5681
+ function isObject3(value2) {
4635
5682
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
4636
5683
  }
4637
5684
  function toolResult(value2, isError = false) {
@@ -4650,7 +5697,7 @@ var PlatformMcpServer = class {
4650
5697
  env;
4651
5698
  initialized = false;
4652
5699
  async handle(message) {
4653
- if (!isObject2(message) || message.jsonrpc !== "2.0" || typeof message.method !== "string" || message.id !== void 0 && typeof message.id !== "string" && typeof message.id !== "number" && message.id !== null) {
5700
+ if (!isObject3(message) || message.jsonrpc !== "2.0" || typeof message.method !== "string" || message.id !== void 0 && typeof message.id !== "string" && typeof message.id !== "number" && message.id !== null) {
4654
5701
  return error(null, -32600, "\uC62C\uBC14\uB978 JSON-RPC \uC694\uCCAD\uC774 \uC544\uB2D9\uB2C8\uB2E4.");
4655
5702
  }
4656
5703
  const request = message;
@@ -4659,7 +5706,7 @@ var PlatformMcpServer = class {
4659
5706
  return void 0;
4660
5707
  }
4661
5708
  if (request.method === "initialize") {
4662
- const requested = isObject2(request.params) && typeof request.params.protocolVersion === "string" ? request.params.protocolVersion : "";
5709
+ const requested = isObject3(request.params) && typeof request.params.protocolVersion === "string" ? request.params.protocolVersion : "";
4663
5710
  if (!requested) return error(request.id, -32602, "\uD504\uB85C\uD1A0\uCF5C \uBC84\uC804\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
4664
5711
  this.initialized = false;
4665
5712
  return { jsonrpc: "2.0", id: request.id, result: {
@@ -4672,7 +5719,7 @@ var PlatformMcpServer = class {
4672
5719
  if (!this.initialized) return error(request.id, -32e3, "MCP \uCD08\uAE30\uD654\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
4673
5720
  if (request.method === "tools/list") return { jsonrpc: "2.0", id: request.id, result: { tools: toolDefinitions } };
4674
5721
  if (request.method !== "tools/call") return error(request.id, -32601, "\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 MCP \uBA54\uC11C\uB4DC\uC785\uB2C8\uB2E4.");
4675
- if (!isObject2(request.params) || typeof request.params.name !== "string" || request.params.arguments !== void 0 && !isObject2(request.params.arguments)) {
5722
+ if (!isObject3(request.params) || typeof request.params.name !== "string" || request.params.arguments !== void 0 && !isObject3(request.params.arguments)) {
4676
5723
  return error(request.id, -32602, "\uB3C4\uAD6C \uC774\uB984\uACFC \uC778\uC790\uB97C \uD655\uC778\uD558\uC138\uC694.");
4677
5724
  }
4678
5725
  const name2 = request.params.name;
@@ -4682,7 +5729,7 @@ var PlatformMcpServer = class {
4682
5729
  const writeKind = name2 === "platform_server_data_put" ? "put" : name2 === "platform_server_data_patch" ? "patch" : name2 === "platform_server_data_delete" ? "delete" : null;
4683
5730
  if (writeKind) {
4684
5731
  const preview = platformWritePreview(writeKind, args);
4685
- if (!isObject2(args) || args.confirm !== true) {
5732
+ if (!isObject3(args) || args.confirm !== true) {
4686
5733
  return { jsonrpc: "2.0", id: request.id, result: toolResult({ preview: true, confirmation_required: true, ...preview }) };
4687
5734
  }
4688
5735
  }
@@ -4700,7 +5747,7 @@ var PlatformMcpServer = class {
4700
5747
  }
4701
5748
  /** 개인 액세스 토큰(platform:read·platform:write, skill:*, creator-content:write·publish 스코프) 기반 도구. */
4702
5749
  async callPatTool(name2, args) {
4703
- if (patDestructiveToolNames.has(name2) && (!isObject2(args) || args.confirm !== true)) {
5750
+ if (patDestructiveToolNames.has(name2) && (!isObject3(args) || args.confirm !== true)) {
4704
5751
  return { preview: true, confirmation_required: true, tool: name2, arguments: redact(args) };
4705
5752
  }
4706
5753
  const client = platformPatClient(this.settings, this.env);
@@ -4715,7 +5762,7 @@ var PlatformMcpServer = class {
4715
5762
  }
4716
5763
  if (name2 === "platform_skill_push") {
4717
5764
  const { dir, skill_id } = parsePat(patSchemas.skillPush, args, "\uC2A4\uD0AC \uC5C5\uB85C\uB4DC \uC785\uB825");
4718
- const prepared = await prepareSkillPackage(dir ?? ".");
5765
+ const prepared = await prepareSkillPackage(dir ?? ".", { fillEditorHash: true });
4719
5766
  if (prepared.manifest.type !== "instruction" && prepared.manifest.type !== "template") {
4720
5767
  throw new CliError("\uD604\uC7AC \uC2A4\uD0AC \uC5C5\uB85C\uB4DC\uB294 instruction\xB7template \uC720\uD615\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4.", "SKILL_TYPE_UNSUPPORTED");
4721
5768
  }
@@ -4865,7 +5912,7 @@ var revision3 = (value2) => parse(z11.coerce.number().int().nonnegative().max(42
4865
5912
  var base = "/v4/creator/custom-pages";
4866
5913
  var presentationSchema = z11.object({
4867
5914
  schema_version: z11.literal(1),
4868
- header_mode: z11.enum(["fixed", "scroll_hide", "translucent_scroll_hide", "floating_close"]),
5915
+ header_mode: z11.enum(["fixed", "scroll_hide", "translucent_scroll_hide", "floating_close", "floating_close_safe_area"]),
4869
5916
  color_scheme: z11.enum(["light", "dark"])
4870
5917
  }).strict();
4871
5918
  function registerPageCommands(program2, runtime2) {
@@ -4877,7 +5924,7 @@ function registerPageCommands(program2, runtime2) {
4877
5924
  const policy = parse(z11.literal("2026-09-22"), opts.policyVersion);
4878
5925
  await mutate(ctx, "POST", base, { target_type: opts.target, ...opts.target === "space" ? { space_id: uuid3(opts.spaceId) } : {}, policy_version: policy });
4879
5926
  });
4880
- runtime2.action(page.command("upload <id> <file>").description("HTML/ZIP \uC5C5\uB85C\uB4DC\uB85C \uC0C8 \uBE44\uACF5\uAC1C \uBC84\uC804 \uC0DD\uC131 (custom-page:write)").addOption(new Option3("--header <mode>", "\uD5E4\uB354 \uD45C\uC2DC").choices(["fixed", "scroll_hide", "translucent_scroll_hide", "floating_close"]).default("fixed")).addOption(new Option3("--color <scheme>", "\uD5E4\uB354 \uC0C9\uC0C1").choices(["light", "dark"]).default("light")), async (ctx, [id2, file], opts) => {
5927
+ runtime2.action(page.command("upload <id> <file>").description("HTML/ZIP \uC5C5\uB85C\uB4DC\uB85C \uC0C8 \uBE44\uACF5\uAC1C \uBC84\uC804 \uC0DD\uC131 (custom-page:write)").addOption(new Option3("--header <mode>", "\uD5E4\uB354 \uD45C\uC2DC").choices(["fixed", "scroll_hide", "translucent_scroll_hide", "floating_close", "floating_close_safe_area"]).default("fixed")).addOption(new Option3("--color <scheme>", "\uD5E4\uB354 \uC0C9\uC0C1").choices(["light", "dark"]).default("light")), async (ctx, [id2, file], opts) => {
4881
5928
  const path = `${base}/${uuid3(id2)}`;
4882
5929
  const prepared = await prepareContentFile(file);
4883
5930
  const presentation = parse(presentationSchema, { schema_version: 1, header_mode: opts.header, color_scheme: opts.color });
@@ -4895,7 +5942,7 @@ function registerPageCommands(program2, runtime2) {
4895
5942
  }
4896
5943
  });
4897
5944
  runtime2.action(page.command("complete <id> <upload-id>").description("\uC5C5\uB85C\uB4DC \uC644\uB8CC \uCC98\uB9AC \uC7AC\uC2DC\uB3C4 (custom-page:write)"), async (ctx, [id2, uploadId]) => mutate(ctx, "POST", `${base}/${uuid3(id2)}/uploads/${uuid3(uploadId)}/complete`, {}));
4898
- runtime2.action(page.command("presentation <id> <version-id>").description("\uD30C\uC77C\uC740 \uB450\uACE0 \uD5E4\uB354 \uC124\uC815\uB9CC \uBC14\uAFBC \uC0C8 \uBC84\uC804 \uC0DD\uC131 (custom-page:write)").addOption(new Option3("--header <mode>", "\uC0C8 \uBC84\uC804\uC758 \uD5E4\uB354 \uD45C\uC2DC").choices(["fixed", "scroll_hide", "translucent_scroll_hide", "floating_close"]).makeOptionMandatory()).addOption(new Option3("--color <scheme>", "\uC0C8 \uBC84\uC804\uC758 \uD5E4\uB354 \uC0C9\uC0C1").choices(["light", "dark"]).makeOptionMandatory()), async (ctx, [id2, versionId], opts) => mutate(ctx, "POST", `${base}/${uuid3(id2)}/versions/${uuid3(versionId)}/presentation`, {
5945
+ runtime2.action(page.command("presentation <id> <version-id>").description("\uD30C\uC77C\uC740 \uB450\uACE0 \uD5E4\uB354 \uC124\uC815\uB9CC \uBC14\uAFBC \uC0C8 \uBC84\uC804 \uC0DD\uC131 (custom-page:write)").addOption(new Option3("--header <mode>", "\uC0C8 \uBC84\uC804\uC758 \uD5E4\uB354 \uD45C\uC2DC").choices(["fixed", "scroll_hide", "translucent_scroll_hide", "floating_close", "floating_close_safe_area"]).makeOptionMandatory()).addOption(new Option3("--color <scheme>", "\uC0C8 \uBC84\uC804\uC758 \uD5E4\uB354 \uC0C9\uC0C1").choices(["light", "dark"]).makeOptionMandatory()), async (ctx, [id2, versionId], opts) => mutate(ctx, "POST", `${base}/${uuid3(id2)}/versions/${uuid3(versionId)}/presentation`, {
4899
5946
  presentation: parse(presentationSchema, { schema_version: 1, header_mode: opts.header, color_scheme: opts.color })
4900
5947
  }));
4901
5948
  runtime2.action(page.command("preview <id> <version-id>").description("\uC571 \uD655\uC778\uC6A9 \uBBF8\uB9AC\uBCF4\uAE30 \uB9C1\uD06C \uC870\uD68C (custom-page:read)"), async (ctx, [id2, versionId]) => ctx.output(await ctx.api.request("GET", `${base}/${uuid3(id2)}/versions/${uuid3(versionId)}/preview`)));