@clack-platform/cli 0.3.0 → 0.3.2

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 +3 -1
  2. package/dist/cli.js +153 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -183,7 +183,7 @@ ZIP에 `clack.content.json` 또는 `.clack/`가 있으면 업로드 전에 선
183
183
 
184
184
  `content update`는 제목·설명·종류·썸네일·메타데이터·태그를 서버 PATCH와 같은 규칙으로 수정합니다(`--title`, `--description`, `--kind`, `--thumbnail-id <썸네일 작업 ID|none>`, `--metadata-mode single|localized`, `--metadata-lang ko|en|none`, `--localized-metadata '<JSON>'|none`, `--tags 쉼표목록`). 지정한 필드만 바뀌고 `--tags`·`--localized-metadata`는 통째 교체됩니다. 단일 메타데이터 모드에서는 제목을 비우면 「제목 없는 콘텐츠」로 표시되고, 언어별(`localized`) 모드에서는 빈 제목을 서버가 거부합니다(400). 썸네일 작업 ID는 `POST /v4/creator/thumbnails` 등으로 발급하며 CLI에는 썸네일 업로드 명령이 없어 보통 `none`(기본 이미지로 되돌리기)만 씁니다. 언어별 메타데이터·태그는 서버 게이트가 켜진 환경에서만 쓸 수 있습니다. 인트로 저장은 개인 액세스 토큰으로 할 수 없어 크리에이터 센터에서만 합니다.
185
185
 
186
- `content info-version <콘텐츠-UUID> <버전-UUID>`는 승인·반려된 버전의 번들을 재사용해 바뀐 콘텐츠 정보만 담은 새 초안 버전을 만듭니다(새로 만들면 201, 이미 있던 초안이면 200이며 응답의 `created`로 구분). 새 버전은 앱 확인과 심사를 다시 받아야 합니다. 서버 게이트가 꺼진 환경에서는 `CONTENT_INFO_VERSION_DISABLED`(409)가 반환되며 `clack content config`의 `info_version.enabled`로 확인할 수 있습니다.
186
+ `content info-version <콘텐츠-UUID> <버전-UUID>`는 승인·반려된 버전의 번들을 재사용해 바뀐 콘텐츠 정보만 담은 새 초안 버전을 만듭니다(새로 만들면 201, 같은 정보의 미게시 버전(초안·심사 중·승인 미게시)이 있으면 200이며 응답의 `created`로 구분). 새 버전은 앱 확인과 심사를 다시 받아야 합니다. 서버 게이트가 꺼진 환경에서는 `CONTENT_INFO_VERSION_DISABLED`(409)가 반환되며 `clack content config`의 `info_version.enabled`로 확인할 수 있습니다.
187
187
 
188
188
  ```bash
189
189
  clack content update --env dev <콘텐츠-UUID> --title "새 제목" --tags 여행,사진
@@ -357,6 +357,8 @@ clack page status PAGE_ID
357
357
  - 업로드: `{"action":"upload","session_id":"UUID","field":"plates","item_index":0,"file":"./portrait.png"}`. 응답 자산 ID를 `plates[0].asset_id`로 저장한다.
358
358
  - 견적: `{"action":"quote","session_id":"UUID"}`. 생성은 `action:"image"` 또는 `"image-batch"`, `form_revision`·`asset_slot`·`approved_price_cash`·`idempotency_key`와 `item_index` 또는 `item_indexes`를 보낸다. 같은 요청의 재시도에는 같은 키를 사용한다.
359
359
  - 가져오기: `{"action":"import","session_id":"UUID","field":"portrait_source","content_id":"UUID","revision":0}`. 본인 콘텐츠에서 매니페스트가 내보내도록 선언한 값만 복사한다.
360
+ - 종료(CLI 0.3.1 이상): `clack authoring complete <세션-UUID> --env dev` 또는 `clack authoring abandon <세션-UUID> --env dev --yes`. JSON 입력은 `{"action":"complete","session_id":"UUID"}`·`{"action":"abandon","session_id":"UUID"}`이며 포기는 비대화형에서 `--yes`가 필요하다. 로컬 MCP는 같은 action을 사용하고 abandon에 `confirm:true`를 지정한다.
361
+ - 완료·포기는 폼(template) 세션만 PAT로 가능하다. 에디터·지침형 스킬의 생성·종료는 403 `SESSION_REQUIRED`이므로 센터에서 작업한다. 본인 세션이 없으면 404 `AUTHORING_NOT_FOUND`다. complete는 현재 revision을 package한 뒤 가능하며(409 `AUTHORING_NOT_PACKAGED`), 종료 재호출은 409 `AUTHORING_SESSION_LOCKED`다. 종료 응답의 `data.status`를 확인한다. 포기해도 기존 콘텐츠는 유지되며 게시·심사는 별도다. 응답 유실 때는 get으로 상태를 조회하고 변경 요청을 자동 재시도하지 않는다.
360
362
  - AI 채우기는 `fill-quote` → `fill` → `fill-status`, 자산 목록은 `assets`, 이미지 작업 조회는 `tool`, 폼 검증은 `validate`를 사용한다.
361
363
 
362
364
  `image_list` 값은 `[{"asset_id":"UUID","scene":"장면","pose":"구도","mood":"분위기","aspect":"1:1"}]`이며 한 번에 최대 8장, 콘텐츠당 최대 24장을 사용한다. 스킬의 `max_calls`와 플랫폼 설정 중 작은 한도를 적용한다. 참조 이미지 생성은 공급자 검증 전 비활성이며 현재 외모·화풍·팔레트 텍스트로 동일성을 안내한다.
package/dist/cli.js CHANGED
@@ -28,12 +28,40 @@ var CliError = class extends Error {
28
28
  return 1;
29
29
  }
30
30
  };
31
+ function describeDetails(details) {
32
+ if (!details || typeof details !== "object" || Array.isArray(details)) return [];
33
+ const d = details;
34
+ const text = (v) => typeof v === "string" && v ? v.replace(/[\p{C}]/gu, "").slice(0, 160) : typeof v === "number" || typeof v === "boolean" ? String(v) : void 0;
35
+ const lines = [];
36
+ const reason = text(d.reason);
37
+ if (reason) lines.push(`\uC0AC\uC720: ${reason}`);
38
+ const file = text(d.file);
39
+ if (file) lines.push(`\uD30C\uC77C: ${file}`);
40
+ if (Array.isArray(d.issues)) {
41
+ for (const issue of d.issues.slice(0, 5)) {
42
+ if (!issue || typeof issue !== "object") continue;
43
+ const i = issue;
44
+ const parts = [["\uD30C\uC77C", i.file], ["\uC704\uCE58", i.pointer], ["\uADDC\uCE59", i.rule], ["\uAC12", i.param]].flatMap(([label, v]) => {
45
+ const t = text(v);
46
+ return t ? [`${label}=${t}`] : [];
47
+ });
48
+ if (parts.length) lines.push(`\uBB38\uC81C: ${parts.join(" ")}`);
49
+ }
50
+ }
51
+ const expected = text(d.expected);
52
+ const actual = text(d.actual);
53
+ if (expected !== void 0 || actual !== void 0) lines.push(`\uAE30\uB300: ${expected ?? "-"} / \uC2E4\uC81C: ${actual ?? "-"}`);
54
+ return lines;
55
+ }
31
56
  function apiError(status, body, retryAfter) {
32
57
  const value2 = body && typeof body === "object" ? body : {};
33
58
  const code = typeof value2.code === "string" ? value2.code : `HTTP_${status}`;
34
59
  let message = typeof value2.message === "string" ? value2.message : `\uC694\uCCAD\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (${status}).`;
35
60
  if (status === 401 && code === "SERVER_KEY_INVALID") message += " \uC13C\uD130\uC5D0\uC11C \uC11C\uBC84 \uD0A4 \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uAC70\uB098 \uC0C8\uB85C \uBC1C\uAE09\xB7\uD68C\uC804\uD558\uC138\uC694.";
36
61
  else if (status === 401) message += " clack login\uC73C\uB85C \uB2E4\uC2DC \uC5F0\uACB0\uD558\uC138\uC694.";
62
+ if (code === "SESSION_REQUIRED") message += " \uC5D0\uB514\uD130\xB7\uC9C0\uCE68\uD615 \uC2A4\uD0AC\uC740 \uD06C\uB9AC\uC5D0\uC774\uD130 \uC13C\uD130\uC5D0\uC11C \uC791\uC5C5\uD558\uC138\uC694. \uD3FC \uC138\uC158\uB9CC PAT\uB85C \uB9CC\uB4E4\uACE0 \uB05D\uB0BC \uC218 \uC788\uC2B5\uB2C8\uB2E4.";
63
+ if (code === "AUTHORING_NOT_PACKAGED") message += " JSON action:package\uB97C clack authoring --input <\uD30C\uC77C>\uB85C \uC2E4\uD589\uD55C \uB4A4 authoring complete <\uC138\uC158-ID>\uB97C \uC2E4\uD589\uD558\uC138\uC694.";
64
+ if (code === "AUTHORING_SESSION_LOCKED") message += " JSON action:get\uC744 clack authoring --input <\uD30C\uC77C>\uB85C \uC2E4\uD589\uD574 \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694. \uC774\uBBF8 \uC885\uB8CC\uB41C \uC138\uC158\uC740 \uB2E4\uC2DC \uD3B8\uC9D1\uD558\uAC70\uB098 \uC885\uB8CC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.";
37
65
  if (code === "IDENTITY_VERIFICATION_REQUIRED") message += " \uC571\uC5D0\uC11C \uBCF8\uC778\uC778\uC99D\uC744 \uC644\uB8CC\uD558\uC138\uC694.";
38
66
  if (code === "SCOPE_DENIED") message += " \uD544\uC694\uD55C \uAD8C\uD55C\uC73C\uB85C \uD1A0\uD070\uC744 \uB2E4\uC2DC \uBC1C\uAE09\uD558\uC138\uC694.";
39
67
  if (code === "SKILL_VERSION_MAJOR_REQUIRED") {
@@ -44,6 +72,9 @@ function apiError(status, body, retryAfter) {
44
72
  if (code === "CONTENT_METADATA_EXTRAS_DISABLED") message += " \uC774 \uD658\uACBD\uC5D0\uC11C\uB294 \uC5B8\uC5B4\uBCC4 \uBA54\uD0C0\uB370\uC774\uD130\xB7\uD0DC\uADF8\uB97C \uC544\uC9C1 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.";
45
73
  if (status === 423 || code === "USER_BANNED") message += " \uACC4\uC815 \uC81C\uD55C \uC0C1\uD0DC\uB97C \uC571\uC5D0\uC11C \uD655\uC778\uD558\uC138\uC694.";
46
74
  if (status === 503 && code.endsWith("DISABLED")) message += " \uD604\uC7AC \uC678\uBD80 \uB3C4\uAD6C \uC811\uADFC\uC774 \uC77C\uC2DC \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4.";
75
+ const located = describeDetails(value2.details);
76
+ if (located.length) message += `
77
+ ${located.map((line) => ` ${line}`).join("\n")}`;
47
78
  const raw = Number(value2.retry_after ?? retryAfter);
48
79
  const retry = Number.isFinite(raw) && raw > 0 ? Math.ceil(raw) : void 0;
49
80
  return new CliError(message, code, status, retry);
@@ -153,7 +184,7 @@ function redact(value2) {
153
184
  // package.json
154
185
  var package_default = {
155
186
  name: "@clack-platform/cli",
156
- version: "0.3.0",
187
+ version: "0.3.2",
157
188
  description: "\uD074\uB799 \uACC4\uC815\uC758 \uC0C1\uD488\xB7\uCF58\uD150\uCE20\xB7\uD504\uB85C\uD544\uC744 \uAD00\uB9AC\uD558\uB294 CLI",
158
189
  type: "module",
159
190
  bin: {
@@ -580,7 +611,9 @@ var authoringInput = z2.discriminatedUnion("action", [
580
611
  z2.strictObject({ action: z2.literal("tool"), ...session, call_id: z2.uuid() }),
581
612
  z2.strictObject({ action: z2.literal("fill"), ...session, revision: revision2, fields: z2.array(field).max(60).optional(), instruction: z2.string().max(4e3).optional(), tier: z2.enum(["economy", "performance"]).optional() }),
582
613
  z2.strictObject({ action: z2.literal("fill-status"), ...session, job_id: z2.uuid() }),
583
- z2.strictObject({ action: z2.literal("package"), ...session, revision: revision2 })
614
+ z2.strictObject({ action: z2.literal("package"), ...session, revision: revision2 }),
615
+ z2.strictObject({ action: z2.literal("complete"), ...session }),
616
+ z2.strictObject({ action: z2.literal("abandon"), ...session, confirm: z2.boolean().optional() })
584
617
  ]);
585
618
  async function authoringRequest(client, raw, surface = "cli") {
586
619
  const result = authoringInput.safeParse(raw);
@@ -592,6 +625,9 @@ async function authoringRequest(client, raw, surface = "cli") {
592
625
  return client.request("POST", base2, { body: { ...body2, surface } });
593
626
  }
594
627
  const path = `${base2}/${input.session_id}`;
628
+ if (input.action === "complete" || input.action === "abandon") {
629
+ return client.request("POST", `${path}/complete`, { body: { outcome: input.action === "complete" ? "completed" : "abandoned" } });
630
+ }
595
631
  if (input.action === "get") return client.request("GET", path);
596
632
  if (input.action === "assets") return client.request("GET", `${path}/assets`);
597
633
  if (input.action === "quote") return client.request("GET", `${path}/tools/image.generate/quote`);
@@ -620,16 +656,27 @@ async function authoringRequest(client, raw, surface = "cli") {
620
656
  return client.request(action === "save" ? "PATCH" : "POST", `${path}/${action === "save" ? "form" : action}`, { body });
621
657
  }
622
658
  function registerAuthoringCommands(program2, runtime2) {
623
- runtime2.action(program2.command("authoring").description("\uACF5\uAC1C \uC2A4\uD0AC\uB85C \uC81C\uC791\xB7\uC790\uC0B0 \uC5C5\uB85C\uB4DC\xB7\uC774\uBBF8\uC9C0 \uC0DD\uC131\xB7\uD328\uD0A4\uC9D5 (creator-content:write)").requiredOption("--input <file>", "action\uACFC \uC694\uCCAD \uAC12\uC774 \uC788\uB294 JSON \uD30C\uC77C"), async (ctx, _args, opts) => {
624
- const input = await readPlatformBodyFile(opts.input);
659
+ const command = program2.command("authoring").description("\uC2A4\uD0AC \uC81C\uC791\xB7\uD328\uD0A4\uC9D5\xB7\uD3FC \uC138\uC158 \uC644\uB8CC/\uD3EC\uAE30 (creator-content:write)").option("--input <file>", "action\uACFC \uC694\uCCAD \uAC12\uC774 \uC788\uB294 JSON \uD30C\uC77C").addHelpText("after", "\n\uD3FC \uC138\uC158\uB9CC PAT\uB85C \uC644\uB8CC\xB7\uD3EC\uAE30\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC5D0\uB514\uD130\xB7\uC9C0\uCE68\uD615\uC740 \uC13C\uD130\uC5D0\uC11C \uC791\uC5C5\uD558\uC138\uC694.\ncomplete\uB294 \uD604\uC7AC revision\uC744 package\uD55C \uB4A4 \uC0AC\uC6A9\uD569\uB2C8\uB2E4. \uC885\uB8CC \uC7AC\uD638\uCD9C\uC740 AUTHORING_SESSION_LOCKED\uC785\uB2C8\uB2E4.");
660
+ const run = async (ctx, raw) => {
661
+ const parsed = authoringInput.safeParse(raw);
662
+ if (!parsed.success) throw new CliError("\uC81C\uC791 \uC791\uC5C5 \uC785\uB825 \uD615\uC2DD\uC744 \uD655\uC778\uD558\uC138\uC694.", "VALIDATION_ERROR");
625
663
  if (ctx.options.dryRun) {
626
- const parsed = authoringInput.safeParse(input);
627
- if (!parsed.success) throw new CliError("\uC81C\uC791 \uC791\uC5C5 \uC785\uB825 \uD615\uC2DD\uC744 \uD655\uC778\uD558\uC138\uC694.");
628
664
  ctx.output({ dry_run: true, input: parsed.data });
629
665
  return;
630
666
  }
631
- ctx.output(await authoringRequest(ctx.api, input));
667
+ if (parsed.data.action === "abandon") await ctx.confirm(`\uC81C\uC791 \uC138\uC158 ${parsed.data.session_id}\uC744 \uD3EC\uAE30\uD569\uB2C8\uB2E4. \uC774\uD6C4 \uD3B8\uC9D1\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uAE30\uC874 \uCF58\uD150\uCE20\uB294 \uC720\uC9C0\uB429\uB2C8\uB2E4.`);
668
+ ctx.output(await authoringRequest(ctx.api, parsed.data));
669
+ };
670
+ runtime2.action(command, async (ctx, _args, opts) => {
671
+ if (!opts.input) throw new CliError("--input <file> \uB610\uB294 complete/abandon <session-id>\uB97C \uC9C0\uC815\uD558\uC138\uC694.");
672
+ await run(ctx, await readPlatformBodyFile(opts.input));
632
673
  });
674
+ for (const action of ["complete", "abandon"]) {
675
+ runtime2.action(command.command(`${action} <session-id>`).description(action === "complete" ? "\uD604\uC7AC \uD328\uD0A4\uC9D5\uB41C \uD3FC \uC138\uC158\uC744 \uC644\uC131 \uC0C1\uD0DC\uB85C \uC885\uB8CC" : "\uD3FC \uC138\uC158 \uD3EC\uAE30 (\uBE44\uB300\uD654\uD615 \uC2E4\uD589\uC740 --yes \uD544\uC694)"), async (ctx, args, opts) => {
676
+ if (opts.input) throw new CliError("\uD558\uC704 \uBA85\uB839\uACFC --input\uC744 \uD568\uAED8 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
677
+ await run(ctx, { action, session_id: args[0] });
678
+ });
679
+ }
633
680
  }
634
681
 
635
682
  // src/cli.ts
@@ -2458,6 +2505,16 @@ function inspectIntroSemantics(skill, form, slots, fail2) {
2458
2505
  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.");
2459
2506
  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.`);
2460
2507
  }
2508
+ let sectionLength = 0;
2509
+ for (const [key, name2] of Object.entries(intro.sections ?? {})) {
2510
+ const field2 = textField(name2);
2511
+ if (!field2 || field2["x-clack-visibility"] !== "public") fail2(`/output/intro/sections/${key}`, "\uC18C\uAC1C \uC139\uC158\uC740 \uACF5\uAC1C \uBB38\uC790\uC5F4(text\xB7textarea) \uD3FC \uD544\uB4DC\uB9CC \uAC00\uB9AC\uD0AC \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
2512
+ else {
2513
+ if (field2.maxLength > 2e3) fail2(`/output/intro/sections/${key}`, "\uC18C\uAC1C \uC139\uC158\uC758 \uCD5C\uB300 \uAE38\uC774\uB294 2,000\uC790\uC785\uB2C8\uB2E4.");
2514
+ sectionLength += field2.maxLength;
2515
+ }
2516
+ }
2517
+ if (sectionLength > 6e3) fail2("/output/intro/sections", "\uC18C\uAC1C \uC139\uC158\uC758 \uCD5C\uB300 \uAE38\uC774 \uD569\uACC4\uB294 6,000\uC790\uC785\uB2C8\uB2E4.");
2461
2518
  const fallback = intro.description_fallback ?? [];
2462
2519
  let composed = 0;
2463
2520
  for (const name2 of fallback) {
@@ -3812,6 +3869,37 @@ var clack_skill_v1_schema_default = {
3812
3869
  minItems: 1,
3813
3870
  maxItems: 8,
3814
3871
  uniqueItems: true
3872
+ },
3873
+ sections: {
3874
+ type: "object",
3875
+ additionalProperties: false,
3876
+ description: "\uACF5\uAC1C \uBB38\uC790\uC5F4 \uD3FC \uD544\uB4DC\uB97C \uC18C\uAC1C \uC139\uC158\uC5D0 \uB9E4\uD551\uD569\uB2C8\uB2E4. \uD56D\uBAA9\uBCC4 \uCD5C\uB300 2,000\uC790, \uC120\uC5B8\uB41C \uD544\uB4DC \uCD5C\uB300 \uAE38\uC774 \uD569\uACC4 6,000\uC790\uC785\uB2C8\uB2E4.",
3877
+ properties: {
3878
+ how_to: {
3879
+ type: "string",
3880
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3881
+ },
3882
+ notice: {
3883
+ type: "string",
3884
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3885
+ },
3886
+ character: {
3887
+ type: "string",
3888
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3889
+ },
3890
+ setting: {
3891
+ type: "string",
3892
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3893
+ },
3894
+ relationship: {
3895
+ type: "string",
3896
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3897
+ },
3898
+ first_scene: {
3899
+ type: "string",
3900
+ pattern: "^[a-z][a-z0-9_-]{0,31}$"
3901
+ }
3902
+ }
3815
3903
  }
3816
3904
  }
3817
3905
  }
@@ -5116,6 +5204,7 @@ var clack_content_v1_schema_default = {
5116
5204
  var MAX_ZIP = 10 * 1024 * 1024;
5117
5205
  var MAX_EXPANDED = 30 * 1024 * 1024;
5118
5206
  var MAX_FILES2 = 500;
5207
+ var MAX_ZIP_ENTRIES = 2e3;
5119
5208
  var textDecoder2 = new TextDecoder("utf-8", { fatal: true });
5120
5209
  var ajv = new Ajv20202({ allErrors: true, strict: false });
5121
5210
  ajv.addSchema(clack_content_v1_schema_default);
@@ -5160,6 +5249,14 @@ function skillPathAllowed(path) {
5160
5249
  if (parts.includes("scripts") || /\.(?:sh|bash|zsh|fish|bat|cmd|ps1|exe|dll|so|dylib|wasm|py|rb|php|jar)$/i.test(path)) return false;
5161
5250
  return true;
5162
5251
  }
5252
+ function isArchiveMetadata(path) {
5253
+ const parts = path.split("/");
5254
+ const name2 = parts.at(-1) ?? "";
5255
+ return parts[0] === "__MACOSX" || name2 === ".DS_Store" || name2.startsWith("._");
5256
+ }
5257
+ function unsafeArchivePath(path) {
5258
+ return path.startsWith("/") || /[\p{Cc}]/u.test(path) || path.split("/").includes("..");
5259
+ }
5163
5260
  async function collectFiles(directory) {
5164
5261
  const files = {};
5165
5262
  let totalBytes = 0;
@@ -5167,6 +5264,7 @@ async function collectFiles(directory) {
5167
5264
  const entries = await readdir(join2(directory, relative), { withFileTypes: true });
5168
5265
  for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
5169
5266
  const path = relative ? `${relative}/${entry.name}` : entry.name;
5267
+ if (isArchiveMetadata(path)) continue;
5170
5268
  if (!skillPathAllowed(path)) invalid2(`\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD328\uD0A4\uC9C0 \uACBD\uB85C: ${path}`);
5171
5269
  if (entry.isSymbolicLink() || !(entry.isDirectory() || entry.isFile())) invalid2(`\uC77C\uBC18 \uD30C\uC77C\xB7\uD3F4\uB354\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4: ${path}`);
5172
5270
  if (entry.isDirectory()) {
@@ -5185,22 +5283,47 @@ async function collectFiles(directory) {
5185
5283
  }
5186
5284
  function inspectZip(bytes2, expectedDirectory, hashMismatch) {
5187
5285
  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.");
5286
+ const raw = [];
5287
+ try {
5288
+ unzipSync2(bytes2, { filter(entry) {
5289
+ if (raw.length >= MAX_ZIP_ENTRIES) invalid2("ZIP \uD56D\uBAA9 \uC218\uAC00 2,000\uAC1C\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
5290
+ const dir = entry.name.endsWith("/");
5291
+ raw.push({ name: dir ? entry.name.slice(0, -1) : entry.name, dir });
5292
+ return false;
5293
+ } });
5294
+ } catch (error2) {
5295
+ if (error2 instanceof CliError) throw error2;
5296
+ return invalid2("ZIP \uAD6C\uC870\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
5297
+ }
5298
+ for (const { name: name2 } of raw) if (unsafeArchivePath(name2)) invalid2(`\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 ZIP \uACBD\uB85C: ${name2.replace(new RegExp("\\p{C}", "gu"), "").slice(0, 160)}`);
5299
+ const kept = raw.filter(({ name: name2 }) => !isArchiveMetadata(name2));
5300
+ const keptFiles = kept.filter(({ dir }) => !dir);
5301
+ if (keptFiles.length > MAX_FILES2) invalid2("\uC2A4\uD0AC \uD30C\uC77C \uC218\uAC00 500\uAC1C\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
5302
+ let prefix = "";
5303
+ const top = keptFiles[0]?.name.split("/")[0];
5304
+ if (top && keptFiles.every(({ name: name2 }) => name2.startsWith(`${top}/`))) {
5305
+ const names = new Set(keptFiles.map(({ name: name2 }) => name2));
5306
+ if (names.has(`${top}/SKILL.md`) && names.has(`${top}/clack.skill.json`)) prefix = `${top}/`;
5307
+ }
5188
5308
  const seen = /* @__PURE__ */ new Set();
5189
- let entries = 0;
5309
+ const wanted = /* @__PURE__ */ new Map();
5310
+ for (const { name: name2, dir } of kept) {
5311
+ if (dir && prefix && name2 === prefix.slice(0, -1)) continue;
5312
+ const path = prefix && name2.startsWith(prefix) ? name2.slice(prefix.length) : name2;
5313
+ if (!skillPathAllowed(path) || seen.has(path.toLowerCase())) invalid2(`\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 ZIP \uACBD\uB85C: ${path}`);
5314
+ seen.add(path.toLowerCase());
5315
+ if (!dir) wanted.set(name2, path);
5316
+ }
5190
5317
  let totalBytes = 0;
5191
- let extracted;
5318
+ const extracted = {};
5192
5319
  try {
5193
- extracted = unzipSync2(bytes2, { filter(entry) {
5194
- entries++;
5195
- if (entries > MAX_FILES2) invalid2("\uC2A4\uD0AC \uD30C\uC77C \uC218\uAC00 500\uAC1C\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
5196
- const path = entry.name.endsWith("/") ? entry.name.slice(0, -1) : entry.name;
5197
- if (!skillPathAllowed(path) || seen.has(path.toLowerCase())) invalid2(`\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 ZIP \uACBD\uB85C: ${path}`);
5198
- seen.add(path.toLowerCase());
5199
- if (entry.name.endsWith("/")) return false;
5320
+ const content = unzipSync2(bytes2, { filter(entry) {
5321
+ if (!wanted.has(entry.name)) return false;
5200
5322
  totalBytes += entry.originalSize;
5201
5323
  if (totalBytes > MAX_EXPANDED) invalid2("\uC2A4\uD0AC \uC555\uCD95 \uD574\uC81C \uC6A9\uB7C9\uC774 30 MiB\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.");
5202
5324
  return true;
5203
5325
  } });
5326
+ for (const [name2, data] of Object.entries(content)) extracted[wanted.get(name2)] = data;
5204
5327
  } catch (error2) {
5205
5328
  if (error2 instanceof CliError) throw error2;
5206
5329
  return invalid2("ZIP \uAD6C\uC870\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
@@ -5443,11 +5566,12 @@ async function pushSkillVersion(client, prepared, skillId) {
5443
5566
  if (error2 instanceof CliError && error2.code === "SKILL_VERSION_MAJOR_REQUIRED") {
5444
5567
  throw new CliError(`${error2.message} (\uCDE8\uC18C: clack skill cancel ${resolvedSkillId} ${session2.version_id})`, error2.code, error2.status);
5445
5568
  }
5446
- if (error2 instanceof CliError) throw new CliError(
5447
- `${error2.message} \uC644\uB8CC \uC7AC\uC2DC\uB3C4: clack skill complete ${resolvedSkillId} ${session2.version_id}`,
5448
- error2.code,
5449
- error2.status
5450
- );
5569
+ if (error2 instanceof CliError) {
5570
+ const judged = error2.status >= 400 && error2.status < 500 && error2.status !== 429;
5571
+ const hint = judged ? "\uD328\uD0A4\uC9C0\uB97C \uACE0\uCE5C \uB4A4 \uAC19\uC740 \uBC84\uC804\uC73C\uB85C \uB2E4\uC2DC clack skill push\uD558\uC138\uC694." : `\uC644\uB8CC \uC7AC\uC2DC\uB3C4: clack skill complete ${resolvedSkillId} ${session2.version_id}`;
5572
+ throw new CliError(`${error2.message}
5573
+ ${hint}`, error2.code, error2.status, error2.retryAfter);
5574
+ }
5451
5575
  throw error2;
5452
5576
  }
5453
5577
  }
@@ -5773,7 +5897,7 @@ function parsePat(schema, input, label) {
5773
5897
  return parsed.data;
5774
5898
  }
5775
5899
  var toolDefinitions = [
5776
- { name: "platform_authoring", description: "\uACF5\uAC1C \uC2A4\uD0AC \uC81C\uC791 \uC138\uC158\xB7\uD3FC\xB7\uC790\uC0B0\xB7\uC774\uBBF8\uC9C0 \uC0DD\uC131\xB7\uD328\uD0A4\uC9D5\uC744 \uCC98\uB9AC\uD569\uB2C8\uB2E4. \uC774\uBBF8\uC9C0 \uC0DD\uC131\uC5D0\uB294 \uACAC\uC801 \uC2B9\uC778 \uAC00\uACA9\uACFC \uBA71\uB4F1\uC131 \uD0A4\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. creator-content:write \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.", inputSchema: { type: "object", ...z10.toJSONSchema(authoringInput) }, annotations: { readOnlyHint: false, destructiveHint: false } },
5900
+ { name: "platform_authoring", description: "\uACF5\uAC1C \uC2A4\uD0AC \uC81C\uC791 \uC138\uC158\xB7\uD3FC\xB7\uC790\uC0B0\xB7\uC774\uBBF8\uC9C0 \uC0DD\uC131\xB7\uD328\uD0A4\uC9D5\xB7\uC644\uB8CC\xB7\uD3EC\uAE30\uB97C \uCC98\uB9AC\uD569\uB2C8\uB2E4. complete/abandon\uC740 \uD3FC \uC138\uC158 \uC804\uC6A9\uC774\uBA70 complete\uB294 \uD604\uC7AC revision \uD328\uD0A4\uC9D5, abandon\uC740 confirm:true\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. \uC5D0\uB514\uD130\xB7\uC9C0\uCE68\uD615\uC740 \uC13C\uD130\uC5D0\uC11C \uC791\uC5C5\uD569\uB2C8\uB2E4. \uC774\uBBF8\uC9C0 \uC0DD\uC131\uC5D0\uB294 \uACAC\uC801 \uC2B9\uC778 \uAC00\uACA9\uACFC \uBA71\uB4F1\uC131 \uD0A4\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. creator-content:write \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.", inputSchema: { type: "object", ...z10.toJSONSchema(authoringInput) }, annotations: { readOnlyHint: false, destructiveHint: true } },
5777
5901
  {
5778
5902
  name: "platform_usage_get",
5779
5903
  description: "\uC11C\uBC84 \uD0A4\uAC00 \uC18D\uD55C \uCF58\uD150\uCE20\uC758 \uD50C\uB7AB\uD3FC \uC0AC\uC6A9\uB7C9\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4.",
@@ -5965,8 +6089,14 @@ var PlatformMcpServer = class {
5965
6089
  if (patDestructiveToolNames.has(name2) && (!isObject3(args) || args.confirm !== true)) {
5966
6090
  return { preview: true, confirmation_required: true, tool: name2, arguments: redact(args) };
5967
6091
  }
6092
+ if (name2 === "platform_authoring") {
6093
+ const input = parsePat(authoringInput, args, "\uC81C\uC791 \uC791\uC5C5 \uC785\uB825");
6094
+ if (input.action === "abandon" && input.confirm !== true) {
6095
+ return { preview: true, confirmation_required: true, tool: name2, arguments: redact(input) };
6096
+ }
6097
+ return authoringRequest(platformPatClient(this.settings, this.env), input, "mcp");
6098
+ }
5968
6099
  const client = platformPatClient(this.settings, this.env);
5969
- if (name2 === "platform_authoring") return authoringRequest(client, args, "mcp");
5970
6100
  if (name2 === "platform_skill_list") {
5971
6101
  const { q } = parsePat(patSchemas.skillList, args, "\uC2A4\uD0AC \uBAA9\uB85D \uC870\uD68C \uC785\uB825");
5972
6102
  return client.request("GET", "/v4/skills", { query: { q } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clack-platform/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "클랙 계정의 상품·콘텐츠·프로필을 관리하는 CLI",
5
5
  "type": "module",
6
6
  "bin": {