@clack-platform/cli 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +14 -3
  2. package/dist/cli.js +254 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -63,7 +63,7 @@ clack login --resume --env dev # 발급 당시와 같은 환경이어야 한
63
63
  | 크리에이터 채널 | `channel get/create/update`, `channel post list/get/create/update/delete`, `channel series list/create/update/delete` |
64
64
  | 이미지 | `upload <파일...>` |
65
65
  | 설정 | `config get`, `config set <키> <값>` |
66
- | HTML 콘텐츠 | `content config/list/status/create/upload/complete/preview/submit/withdraw/publish/unpublish` |
66
+ | HTML 콘텐츠 | `content config/list/status/create/update/upload/complete/info-version/preview/submit/withdraw/publish/unpublish` |
67
67
  | 에이전트 연결 | `mcp config`, `skills` |
68
68
  | 스킬 패키지·관리 | `skill validate`, `skill pack`, `skill editor dev`, `skill push`, `skill complete`, `skill submit`, `skill list`, `skill get`, `skill form`, `skill status`, `skill cancel`, `skill release`, `skill deprecate` |
69
69
  | 콘텐츠 서버 키(platform:read/write) | `content server-keys list/revoke` (발급·회전은 크리에이터 센터 전용) |
@@ -138,7 +138,7 @@ clack skills --json
138
138
 
139
139
  환경변수 참조 형식은 [Claude Code MCP 문서](https://code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcp-json)와 [Codex MCP 문서](https://developers.openai.com/codex/mcp/)를 따른다. 다른 클라이언트는 해당 제품의 Streamable HTTP·인증 헤더 지원을 확인한다.
140
140
 
141
- 원격 MCP는 프로필·상품·게시글·채널 도구 외에 스킬 목록·상세·업로드(`create_skill`→`request_skill_version_upload`→`complete_skill_version`)·심사 제출·공개(`submit_skill_version`, `release_skill_version`, `get_skill_version`), 승인된 HTML 콘텐츠 공개(`publish_creator_content`), 공유 문서 조회·숨김·삭제(`list_shared_documents`, `hide_shared_document`, `delete_shared_document` 등)도 제공한다. 각 도구는 해당 스코프(`skill:*`, `creator-content:publish`, `platform:read`/`platform:write`)를 가진 토큰에만 노출되며, 파괴적 도구는 `confirm:true` 전에는 미리보기만 반환한다. 원격 MCP는 로컬 파일을 읽지 못하므로 스킬 ZIP 전송은 `clack skill push` 또는 로컬 플랫폼 MCP의 `platform_skill_push`를 사용한다.
141
+ 원격 MCP는 프로필·상품·게시글·채널 도구 외에 스킬 목록·상세·업로드(`create_skill`→`request_skill_version_upload`→`complete_skill_version`)·심사 제출·공개(`submit_skill_version`, `release_skill_version`, `get_skill_version`), HTML 콘텐츠 정보 수정·정보만 새 버전(`update_creator_content`, `create_creator_info_version`), 승인된 HTML 콘텐츠 공개(`publish_creator_content`), 공유 문서 조회·숨김·삭제(`list_shared_documents`, `hide_shared_document`, `delete_shared_document` 등)도 제공한다. 각 도구는 해당 스코프(`skill:*`, `creator-content:write`·`creator-content:publish`, `platform:read`/`platform:write`)를 가진 토큰에만 노출되며, 파괴적 도구는 `confirm:true` 전에는 미리보기만 반환한다. 원격 MCP는 로컬 파일을 읽지 못하므로 스킬 ZIP 전송은 `clack skill push` 또는 로컬 플랫폼 MCP의 `platform_skill_push`를 사용한다.
142
142
 
143
143
  에이전트 스킬은 별도 공개 저장소 [clack-project/skills](https://github.com/clack-project/skills)에서 `npx skills add clack-project/skills`로 설치한다. 모두 10종으로, 일반 계정 관리용 6종(`clack-setup`·`clack-products`·`clack-content`·`clack-channel`·`clack-profile`·`clack-mcp`)과 크리에이터용 4종(`clack-creator-content`·`clack-page`·`clack-skill-package`·`clack-platform-data`)이다. 스킬에 필요한 CLI는 0.1.1 이상이다(`clack-mcp`는 CLI 없이도 쓸 수 있고, 설정 출력에 CLI를 쓸 때는 0.1.0 이상). `clack skills`는 설치를 실행하지 않고 안내만 출력한다. 스킬 목록과 스킬별 요구 CLI 버전도 함께 보여 준다. 스킬 하나만 선택 설치할 때는 `-s clack-products`, 여러 개는 `--skill clack-setup clack-creator-content clack-page`처럼 나열하고, 전역 설치에는 `-g`를 사용한다.
144
144
 
@@ -162,7 +162,7 @@ GitHub Actions는 Linux·macOS·Windows × Node 20·22에서 `pnpm check`와 패
162
162
  |---|---|
163
163
  | `config` | 없음(로그인 불필요) |
164
164
  | `list`, `status`, `preview` | `creator-content:read` |
165
- | `create`, `upload`, `complete`, `withdraw` | `creator-content:write` |
165
+ | `create`, `update`, `upload`, `complete`, `info-version`, `withdraw` | `creator-content:write` |
166
166
  | `submit` | `creator-content:write`와 `creator-content:publish` 모두 |
167
167
  | `publish`, `unpublish` | `creator-content:publish` |
168
168
 
@@ -181,6 +181,15 @@ clack content preview --env dev <콘텐츠-UUID> <버전-UUID>
181
181
 
182
182
  ZIP에 `clack.content.json` 또는 `.clack/`가 있으면 업로드 전에 선언·경로·크기와 프로필 파일 대응을 확인하고 표준 오류에 경고합니다. 선언이 없는 기존 정적 ZIP도 계속 업로드할 수 있습니다. 경고는 업로드를 막지 않으며 서버의 최종 검증·심사 결과를 대신하지 않습니다. 전송 없이 확인하려면 `content upload ... --dry-run`을 사용하세요.
183
183
 
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
+
186
+ `content info-version <콘텐츠-UUID> <버전-UUID>`는 승인·반려된 버전의 번들을 재사용해 바뀐 콘텐츠 정보만 담은 새 초안 버전을 만듭니다(새로 만들면 201, 같은 정보의 미게시 버전(초안·심사 중·승인 미게시)이 있으면 200이며 응답의 `created`로 구분). 새 버전은 앱 확인과 심사를 다시 받아야 합니다. 서버 게이트가 꺼진 환경에서는 `CONTENT_INFO_VERSION_DISABLED`(409)가 반환되며 `clack content config`의 `info_version.enabled`로 확인할 수 있습니다.
187
+
188
+ ```bash
189
+ clack content update --env dev <콘텐츠-UUID> --title "새 제목" --tags 여행,사진
190
+ clack content info-version --env dev <콘텐츠-UUID> <버전-UUID>
191
+ ```
192
+
184
193
  심사는 승인됐지만 아직 공개하지 않은 버전(크리에이터 센터 등록·게시 중단 후 재공개 등)은 `creator-content:publish` 권한으로 공개합니다. 승인 이력·심사 해시·현재 계정과 권한을 공개 시점에 다시 검사합니다.
185
194
 
186
195
  ```bash
@@ -348,6 +357,8 @@ clack page status PAGE_ID
348
357
  - 업로드: `{"action":"upload","session_id":"UUID","field":"plates","item_index":0,"file":"./portrait.png"}`. 응답 자산 ID를 `plates[0].asset_id`로 저장한다.
349
358
  - 견적: `{"action":"quote","session_id":"UUID"}`. 생성은 `action:"image"` 또는 `"image-batch"`, `form_revision`·`asset_slot`·`approved_price_cash`·`idempotency_key`와 `item_index` 또는 `item_indexes`를 보낸다. 같은 요청의 재시도에는 같은 키를 사용한다.
350
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으로 상태를 조회하고 변경 요청을 자동 재시도하지 않는다.
351
362
  - AI 채우기는 `fill-quote` → `fill` → `fill-status`, 자산 목록은 `assets`, 이미지 작업 조회는 `tool`, 폼 검증은 `validate`를 사용한다.
352
363
 
353
364
  `image_list` 값은 `[{"asset_id":"UUID","scene":"장면","pose":"구도","mood":"분위기","aspect":"1:1"}]`이며 한 번에 최대 8장, 콘텐츠당 최대 24장을 사용한다. 스킬의 `max_calls`와 플랫폼 설정 중 작은 한도를 적용한다. 참조 이미지 생성은 공급자 검증 전 비활성이며 현재 외모·화풍·팔레트 텍스트로 동일성을 안내한다.
package/dist/cli.js CHANGED
@@ -34,11 +34,17 @@ function apiError(status, body, retryAfter) {
34
34
  let message = typeof value2.message === "string" ? value2.message : `\uC694\uCCAD\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (${status}).`;
35
35
  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
36
  else if (status === 401) message += " clack login\uC73C\uB85C \uB2E4\uC2DC \uC5F0\uACB0\uD558\uC138\uC694.";
37
+ 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.";
38
+ 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.";
39
+ 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
40
  if (code === "IDENTITY_VERIFICATION_REQUIRED") message += " \uC571\uC5D0\uC11C \uBCF8\uC778\uC778\uC99D\uC744 \uC644\uB8CC\uD558\uC138\uC694.";
38
41
  if (code === "SCOPE_DENIED") message += " \uD544\uC694\uD55C \uAD8C\uD55C\uC73C\uB85C \uD1A0\uD070\uC744 \uB2E4\uC2DC \uBC1C\uAE09\uD558\uC138\uC694.";
39
42
  if (code === "SKILL_VERSION_MAJOR_REQUIRED") {
40
43
  message += " clack.skill.json\xB7SKILL.md\uC758 version\uC744 \uC62C\uB824 \uB2E4\uC2DC push\uD558\uC138\uC694. \uAC19\uC740 \uBC84\uC804\uC744 \uD638\uD658\uB418\uAC8C \uACE0\uCCD0 \uC62C\uB9AC\uB824\uBA74 \uBA3C\uC800 clack skill cancel\uB85C \uC774 \uBC84\uC804\uC744 \uC9C0\uC6B0\uC138\uC694.";
41
44
  }
45
+ if (code === "CONTENT_INFO_VERSION_DISABLED") message += " clack content config\uC758 info_version.enabled\uAC00 true\uC778 \uD658\uACBD\uC5D0\uC11C\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.";
46
+ if (code === "CONTENT_INFO_UNCHANGED") message += " \uC815\uBCF4\uB97C \uBA3C\uC800 clack content update\uB85C \uC218\uC815\uD55C \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.";
47
+ 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.";
42
48
  if (status === 423 || code === "USER_BANNED") message += " \uACC4\uC815 \uC81C\uD55C \uC0C1\uD0DC\uB97C \uC571\uC5D0\uC11C \uD655\uC778\uD558\uC138\uC694.";
43
49
  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.";
44
50
  const raw = Number(value2.retry_after ?? retryAfter);
@@ -150,7 +156,7 @@ function redact(value2) {
150
156
  // package.json
151
157
  var package_default = {
152
158
  name: "@clack-platform/cli",
153
- version: "0.2.0",
159
+ version: "0.3.1",
154
160
  description: "\uD074\uB799 \uACC4\uC815\uC758 \uC0C1\uD488\xB7\uCF58\uD150\uCE20\xB7\uD504\uB85C\uD544\uC744 \uAD00\uB9AC\uD558\uB294 CLI",
155
161
  type: "module",
156
162
  bin: {
@@ -577,7 +583,9 @@ var authoringInput = z2.discriminatedUnion("action", [
577
583
  z2.strictObject({ action: z2.literal("tool"), ...session, call_id: z2.uuid() }),
578
584
  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() }),
579
585
  z2.strictObject({ action: z2.literal("fill-status"), ...session, job_id: z2.uuid() }),
580
- z2.strictObject({ action: z2.literal("package"), ...session, revision: revision2 })
586
+ z2.strictObject({ action: z2.literal("package"), ...session, revision: revision2 }),
587
+ z2.strictObject({ action: z2.literal("complete"), ...session }),
588
+ z2.strictObject({ action: z2.literal("abandon"), ...session, confirm: z2.boolean().optional() })
581
589
  ]);
582
590
  async function authoringRequest(client, raw, surface = "cli") {
583
591
  const result = authoringInput.safeParse(raw);
@@ -589,6 +597,9 @@ async function authoringRequest(client, raw, surface = "cli") {
589
597
  return client.request("POST", base2, { body: { ...body2, surface } });
590
598
  }
591
599
  const path = `${base2}/${input.session_id}`;
600
+ if (input.action === "complete" || input.action === "abandon") {
601
+ return client.request("POST", `${path}/complete`, { body: { outcome: input.action === "complete" ? "completed" : "abandoned" } });
602
+ }
592
603
  if (input.action === "get") return client.request("GET", path);
593
604
  if (input.action === "assets") return client.request("GET", `${path}/assets`);
594
605
  if (input.action === "quote") return client.request("GET", `${path}/tools/image.generate/quote`);
@@ -617,16 +628,27 @@ async function authoringRequest(client, raw, surface = "cli") {
617
628
  return client.request(action === "save" ? "PATCH" : "POST", `${path}/${action === "save" ? "form" : action}`, { body });
618
629
  }
619
630
  function registerAuthoringCommands(program2, runtime2) {
620
- 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) => {
621
- const input = await readPlatformBodyFile(opts.input);
631
+ 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.");
632
+ const run = async (ctx, raw) => {
633
+ const parsed = authoringInput.safeParse(raw);
634
+ if (!parsed.success) throw new CliError("\uC81C\uC791 \uC791\uC5C5 \uC785\uB825 \uD615\uC2DD\uC744 \uD655\uC778\uD558\uC138\uC694.", "VALIDATION_ERROR");
622
635
  if (ctx.options.dryRun) {
623
- const parsed = authoringInput.safeParse(input);
624
- if (!parsed.success) throw new CliError("\uC81C\uC791 \uC791\uC5C5 \uC785\uB825 \uD615\uC2DD\uC744 \uD655\uC778\uD558\uC138\uC694.");
625
636
  ctx.output({ dry_run: true, input: parsed.data });
626
637
  return;
627
638
  }
628
- ctx.output(await authoringRequest(ctx.api, input));
639
+ 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.`);
640
+ ctx.output(await authoringRequest(ctx.api, parsed.data));
641
+ };
642
+ runtime2.action(command, async (ctx, _args, opts) => {
643
+ if (!opts.input) throw new CliError("--input <file> \uB610\uB294 complete/abandon <session-id>\uB97C \uC9C0\uC815\uD558\uC138\uC694.");
644
+ await run(ctx, await readPlatformBodyFile(opts.input));
629
645
  });
646
+ for (const action of ["complete", "abandon"]) {
647
+ 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) => {
648
+ if (opts.input) throw new CliError("\uD558\uC704 \uBA85\uB839\uACFC --input\uC744 \uD568\uAED8 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
649
+ await run(ctx, { action, session_id: args[0] });
650
+ });
651
+ }
630
652
  }
631
653
 
632
654
  // src/cli.ts
@@ -2149,6 +2171,55 @@ var createSchema = z8.object({
2149
2171
  kind: z8.enum(["html", "gallery", "slideshow", "video"]).default("html"),
2150
2172
  policy_version: z8.literal("2026-09-22")
2151
2173
  }).strict();
2174
+ var METADATA_LANGS = ["ko", "en"];
2175
+ var TAG_PATTERN = /^[\p{L}\p{M}\p{N}_]{1,30}$/u;
2176
+ var localizedEntry = z8.object({
2177
+ title: z8.string().trim().max(120),
2178
+ description: z8.string().trim().max(2e3).optional()
2179
+ }).strict();
2180
+ var updateSchema = z8.object({
2181
+ title: z8.string().trim().max(120).optional(),
2182
+ description: z8.string().trim().max(2e3).optional(),
2183
+ kind: z8.enum(["html", "gallery", "slideshow", "video"]).optional(),
2184
+ thumbnail_id: z8.uuid().nullable().optional(),
2185
+ metadata_mode: z8.enum(["single", "localized"]).optional(),
2186
+ metadata_lang: z8.enum(METADATA_LANGS).nullable().optional(),
2187
+ localized_metadata: z8.partialRecord(z8.enum(METADATA_LANGS), localizedEntry).nullable().optional(),
2188
+ tags: z8.array(z8.string().max(64)).max(50).optional()
2189
+ }).strict().superRefine((value2, ctx) => {
2190
+ const issue = (message) => ctx.addIssue({ code: "custom", message });
2191
+ if (!Object.values(value2).some((entry) => entry !== void 0)) issue("\uBCC0\uACBD\uD560 \uD544\uB4DC\uB97C \uD558\uB098 \uC774\uC0C1 \uC9C0\uC815\uD558\uC138\uC694.");
2192
+ if (value2.tags) {
2193
+ const seen = /* @__PURE__ */ new Set();
2194
+ for (const raw of value2.tags) {
2195
+ const tag = raw.trim().replace(/^#+/, "").trim().normalize("NFC");
2196
+ if (!TAG_PATTERN.test(tag)) issue("\uD574\uC2DC\uD0DC\uADF8\uB294 \uAE00\uC790\xB7\uC22B\uC790\xB7\uBC11\uC904(_)\uB9CC \uC0AC\uC6A9\uD574 1~30\uC790\uB85C \uC785\uB825\uD558\uC138\uC694.");
2197
+ const key = tag.normalize("NFKC").toLowerCase();
2198
+ if (key.length > 40) issue("\uD574\uC2DC\uD0DC\uADF8\uAC00 \uB108\uBB34 \uAE41\uB2C8\uB2E4. \uB354 \uC9E7\uAC8C \uC785\uB825\uD558\uC138\uC694.");
2199
+ seen.add(key);
2200
+ }
2201
+ if (seen.size > 10) issue("\uD574\uC2DC\uD0DC\uADF8\uB294 \uCD5C\uB300 10\uAC1C\uAE4C\uC9C0 \uB4F1\uB85D\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
2202
+ }
2203
+ if (value2.metadata_mode === "localized") {
2204
+ if (!value2.metadata_lang) issue("\uC5B8\uC5B4\uBCC4\uB85C \uC785\uB825\uD558\uB824\uBA74 --metadata-lang\uC73C\uB85C \uB300\uD45C \uC5B8\uC5B4\uB97C \uC9C0\uC815\uD558\uC138\uC694.");
2205
+ if (!value2.localized_metadata) issue("\uC5B8\uC5B4\uBCC4\uB85C \uC785\uB825\uD558\uB824\uBA74 --localized-metadata\uB85C \uB300\uD45C \uC5B8\uC5B4 \uC678 \uC5B8\uC5B4\uB97C 1\uAC1C \uC774\uC0C1 \uC9C0\uC815\uD558\uC138\uC694.");
2206
+ }
2207
+ if (value2.localized_metadata) {
2208
+ for (const [lang, entry] of Object.entries(value2.localized_metadata)) {
2209
+ if (entry && !entry.title.trim()) issue("\uC120\uD0DD\uD55C \uBAA8\uB4E0 \uC5B8\uC5B4\uC758 \uC81C\uBAA9\uC744 \uC785\uB825\uD558\uC138\uC694.");
2210
+ if (lang === value2.metadata_lang) issue("\uB300\uD45C \uC5B8\uC5B4\uB294 --title\xB7--description\uC5D0 \uB123\uACE0, \uC5B8\uC5B4\uBCC4 \uAC12\uC5D0\uB294 \uB2E4\uB978 \uC5B8\uC5B4\uB9CC \uB123\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
2211
+ }
2212
+ if (value2.metadata_mode === "single" && Object.keys(value2.localized_metadata).length) issue("\uD55C \uC5B8\uC5B4(single)\uB85C \uC785\uB825\uD560 \uB54C\uB294 \uC5B8\uC5B4\uBCC4 \uAC12\uC744 \uBCF4\uB0BC \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2213
+ }
2214
+ });
2215
+ function jsonOption(name2, value2) {
2216
+ if (value2 === "none") return null;
2217
+ try {
2218
+ return JSON.parse(value2);
2219
+ } catch {
2220
+ throw new CliError(`${name2} \uAC12\uC740 JSON \uAC1D\uCCB4\uC774\uAC70\uB098 none\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.`);
2221
+ }
2222
+ }
2152
2223
  async function prepareContentFile(source) {
2153
2224
  const file = resolve6(source);
2154
2225
  const extension = extname(file).toLowerCase();
@@ -2218,6 +2289,19 @@ function registerContentCommands(program2, runtime2) {
2218
2289
  const body = parse(createSchema, { title: opts.title, description: opts.description, kind: opts.kind, policy_version: opts.policyVersion });
2219
2290
  await mutate(ctx, "POST", "/v4/creator/contents", body);
2220
2291
  });
2292
+ runtime2.action(content.command("update <id>").description("\uCF58\uD150\uCE20 \uC815\uBCF4(\uC81C\uBAA9\xB7\uC124\uBA85\xB7\uC885\uB958\xB7\uC378\uB124\uC77C\xB7\uBA54\uD0C0\uB370\uC774\uD130\xB7\uD0DC\uADF8) \uC218\uC815 (creator-content:write)").option("--title <text>", "\uC81C\uBAA9 (120\uC790 \uC774\uD558, \uBE48 \uAC12\uC774\uBA74 \uC81C\uBAA9 \uC5C6\uC74C)").option("--description <text>", "\uC124\uBA85 (2000\uC790 \uC774\uD558)").addOption(new Option("--kind <kind>", "\uCF58\uD150\uCE20 \uC885\uB958").choices(["html", "gallery", "slideshow", "video"])).option("--thumbnail-id <uuid|none>", "\uC378\uB124\uC77C \uC791\uC5C5 ID, none\uC774\uBA74 \uAE30\uBCF8 \uC774\uBBF8\uC9C0\uB85C \uB418\uB3CC\uB9BC").addOption(new Option("--metadata-mode <mode>", "\uBA54\uD0C0\uB370\uC774\uD130 \uC785\uB825 \uBC29\uC2DD").choices(["single", "localized"])).option("--metadata-lang <ko|en|none>", "\uB300\uD45C \uC5B8\uC5B4").option("--localized-metadata <json|none>", `\uB300\uD45C \uC5B8\uC5B4 \uC678 \uC5B8\uC5B4\uBCC4 \uC81C\uBAA9\xB7\uC124\uBA85 JSON, \uC608: '{"en":{"title":"Title","description":"About"}}'`).option("--tags <list>", "\uD574\uC2DC\uD0DC\uADF8\uB97C \uC27C\uD45C\uB85C \uAD6C\uBD84\uD574 \uD1B5\uC9F8 \uAD50\uCCB4, \uBE48 \uBB38\uC790\uC5F4\uC774\uBA74 \uBAA8\uB450 \uC0AD\uC81C"), async (ctx, [id2], opts) => {
2293
+ const body = parse(updateSchema, {
2294
+ title: opts.title,
2295
+ description: opts.description,
2296
+ kind: opts.kind,
2297
+ thumbnail_id: opts.thumbnailId === void 0 ? void 0 : opts.thumbnailId === "none" ? null : opts.thumbnailId,
2298
+ metadata_mode: opts.metadataMode,
2299
+ metadata_lang: opts.metadataLang === void 0 ? void 0 : opts.metadataLang === "none" ? null : opts.metadataLang,
2300
+ localized_metadata: opts.localizedMetadata === void 0 ? void 0 : jsonOption("--localized-metadata", opts.localizedMetadata),
2301
+ tags: opts.tags === void 0 ? void 0 : csv(opts.tags)
2302
+ });
2303
+ await mutate(ctx, "PATCH", `/v4/creator/contents/${uuid(id2)}`, body);
2304
+ });
2221
2305
  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) => {
2222
2306
  const path = `/v4/creator/contents/${uuid(id2)}`;
2223
2307
  const prepared = await prepareContentFile(file);
@@ -2238,6 +2322,13 @@ function registerContentCommands(program2, runtime2) {
2238
2322
  runtime2.action(content.command("preview <id> <version-id>").description("\uC571 \uD655\uC778\uC6A9 \uBBF8\uB9AC\uBCF4\uAE30 \uB9C1\uD06C \uC870\uD68C (creator-content:read)"), async (ctx, [id2, versionId]) => ctx.output(await ctx.api.request("GET", `/v4/creator/contents/${uuid(id2)}/versions/${uuid(versionId)}/preview`)));
2239
2323
  runtime2.action(content.command("submit <id> <version-id>").description("\uC571\uC5D0\uC11C \uD655\uC778 \uC644\uB8CC\uD55C \uBC84\uC804\uC758 \uC2EC\uC0AC \uC81C\uCD9C, \uD1B5\uACFC \uC2DC \uACF5\uAC1C (creator-content:write\uC640 creator-content:publish \uB458 \uB2E4 \uD544\uC694)"), async (ctx, [id2, versionId]) => mutate(ctx, "POST", `/v4/creator/contents/${uuid(id2)}/versions/${uuid(versionId)}/submit`, { publish_on_approval: true }, "\uC774 \uBC84\uC804\uC744 \uC2EC\uC0AC\uD558\uACE0 \uD1B5\uACFC\uD558\uBA74 \uACF5\uAC1C\uD560\uAE4C\uC694?"));
2240
2324
  runtime2.action(content.command("withdraw <id> <version-id>").description("\uC9C4\uD589 \uC911\uC778 \uC2EC\uC0AC \uCDE8\uC18C (creator-content:write)"), async (ctx, [id2, versionId]) => mutate(ctx, "POST", `/v4/creator/contents/${uuid(id2)}/versions/${uuid(versionId)}/review/cancel`, void 0, "\uC774 \uBC84\uC804\uC758 \uC2EC\uC0AC\uB97C \uCDE8\uC18C\uD560\uAE4C\uC694?"));
2325
+ runtime2.action(content.command("info-version <id> <version-id>").description("\uBC14\uB010 \uCF58\uD150\uCE20 \uC815\uBCF4\uB9CC \uB2F4\uC740 \uC0C8 \uBC84\uC804 \uC0DD\uC131 (creator-content:write)"), async (ctx, [id2, versionId]) => mutate(
2326
+ ctx,
2327
+ "POST",
2328
+ `/v4/creator/contents/${uuid(id2)}/versions/${uuid(versionId)}/info-version`,
2329
+ void 0,
2330
+ "\uD604\uC7AC \uCF58\uD150\uCE20 \uC815\uBCF4\uB85C \uC774 \uBC84\uC804\uC744 \uBCF5\uC81C\uD55C \uC0C8 \uCD08\uC548\uC744 \uB9CC\uB4E4\uAE4C\uC694? \uC0C8 \uBC84\uC804\uC740 \uC571 \uD655\uC778\uACFC \uC2EC\uC0AC\uB97C \uB2E4\uC2DC \uBC1B\uC544\uC57C \uD569\uB2C8\uB2E4."
2331
+ ));
2241
2332
  runtime2.action(content.command("publish <id> <version-id>").description("\uC2B9\uC778\uB410\uC9C0\uB9CC \uACF5\uAC1C\uD558\uC9C0 \uC54A\uC740 \uBC84\uC804 \uACF5\uAC1C (creator-content:publish)"), async (ctx, [id2, versionId]) => mutate(ctx, "POST", `/v4/creator/contents/${uuid(id2)}/versions/${uuid(versionId)}/publish`, void 0, "\uC2B9\uC778\uB41C \uC774 \uBC84\uC804\uC744 \uACF5\uAC1C\uD560\uAE4C\uC694?"));
2242
2333
  runtime2.action(content.command("unpublish <id>").description("\uCF58\uD150\uCE20 \uAC8C\uC2DC \uC911\uB2E8 (creator-content:publish)"), async (ctx, [id2]) => mutate(ctx, "POST", `/v4/creator/contents/${uuid(id2)}/unpublish`, void 0, "\uCF58\uD150\uCE20 \uAC8C\uC2DC\uB97C \uC911\uB2E8\uD560\uAE4C\uC694?"));
2243
2334
  const serverKeys = content.command("server-keys").description("\uCF58\uD150\uCE20 \uC11C\uBC84 \uD0A4 \uC870\uD68C\xB7\uD3D0\uAE30 (platform:read\xB7platform:write, \uBC1C\uAE09\xB7\uD68C\uC804\uC740 \uD06C\uB9AC\uC5D0\uC774\uD130 \uC13C\uD130)");
@@ -2351,9 +2442,15 @@ var INTRO_MAX_IMAGES = 8;
2351
2442
  var INTRO_CREATOR_COMMENT_MAX = 500;
2352
2443
  var INTRO_DESCRIPTION_MAX = 5e3;
2353
2444
  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;
2445
+ var labelRecord = (value2) => value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
2446
+ var introLabelText = (value2) => value2.normalize("NFC").trim();
2447
+ function introFieldLabelMaxLength(name2, field2) {
2448
+ const title = typeof field2?.title === "string" ? introLabelText(field2.title) : "";
2449
+ const lengths = [(title || name2).length];
2450
+ for (const value2 of Object.values(labelRecord(field2?.["x-clack-i18n"]) ?? {})) {
2451
+ if (typeof value2 === "string") lengths.push(introLabelText(value2).length);
2452
+ }
2453
+ return Math.max(...lengths);
2357
2454
  }
2358
2455
  function inspectIntroSemantics(skill, form, slots, fail2) {
2359
2456
  const intro = skill.output?.intro;
@@ -2388,7 +2485,7 @@ function inspectIntroSemantics(skill, form, slots, fail2) {
2388
2485
  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
2486
  continue;
2390
2487
  }
2391
- composed += introFieldLabel(name2, field2).length + 1 + field2.maxLength;
2488
+ composed += introFieldLabelMaxLength(name2, field2) + 1 + field2.maxLength;
2392
2489
  }
2393
2490
  composed += Math.max(0, fallback.length - 1) * 2;
2394
2491
  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.`);
@@ -3068,13 +3165,74 @@ var clack_skill_v1_schema_default = {
3068
3165
  },
3069
3166
  maxItems: 10,
3070
3167
  uniqueItems: true
3168
+ },
3169
+ description: {
3170
+ type: "object",
3171
+ properties: {
3172
+ ko: {
3173
+ type: "string",
3174
+ minLength: 1,
3175
+ maxLength: 1e3
3176
+ },
3177
+ en: {
3178
+ type: "string",
3179
+ minLength: 1,
3180
+ maxLength: 1e3
3181
+ }
3182
+ },
3183
+ required: [
3184
+ "ko"
3185
+ ],
3186
+ additionalProperties: false
3187
+ },
3188
+ tags_i18n: {
3189
+ type: "object",
3190
+ properties: {
3191
+ en: {
3192
+ type: "array",
3193
+ items: {
3194
+ type: "string",
3195
+ minLength: 1,
3196
+ maxLength: 40
3197
+ },
3198
+ minItems: 1,
3199
+ maxItems: 10,
3200
+ uniqueItems: true
3201
+ }
3202
+ },
3203
+ minProperties: 1,
3204
+ additionalProperties: false
3205
+ },
3206
+ release_notes: {
3207
+ type: "object",
3208
+ properties: {
3209
+ ko: {
3210
+ type: "string",
3211
+ minLength: 1,
3212
+ maxLength: 1e3
3213
+ },
3214
+ en: {
3215
+ type: "string",
3216
+ minLength: 1,
3217
+ maxLength: 1e3
3218
+ }
3219
+ },
3220
+ required: [
3221
+ "ko"
3222
+ ],
3223
+ additionalProperties: false
3071
3224
  }
3072
3225
  },
3073
3226
  required: [
3074
3227
  "title",
3075
3228
  "summary",
3076
3229
  "category"
3077
- ]
3230
+ ],
3231
+ dependentRequired: {
3232
+ tags_i18n: [
3233
+ "tags"
3234
+ ]
3235
+ }
3078
3236
  },
3079
3237
  authoring: {
3080
3238
  type: "object",
@@ -4236,6 +4394,44 @@ var authoring_form_v1_schema_default = {
4236
4394
  ],
4237
4395
  additionalProperties: false
4238
4396
  },
4397
+ "x-clack-help-i18n": {
4398
+ type: "object",
4399
+ properties: {
4400
+ ko: {
4401
+ type: "string",
4402
+ minLength: 1,
4403
+ maxLength: 1e3
4404
+ },
4405
+ en: {
4406
+ type: "string",
4407
+ minLength: 1,
4408
+ maxLength: 1e3
4409
+ }
4410
+ },
4411
+ required: [
4412
+ "ko"
4413
+ ],
4414
+ additionalProperties: false
4415
+ },
4416
+ "x-clack-placeholder-i18n": {
4417
+ type: "object",
4418
+ properties: {
4419
+ ko: {
4420
+ type: "string",
4421
+ minLength: 1,
4422
+ maxLength: 300
4423
+ },
4424
+ en: {
4425
+ type: "string",
4426
+ minLength: 1,
4427
+ maxLength: 300
4428
+ }
4429
+ },
4430
+ required: [
4431
+ "ko"
4432
+ ],
4433
+ additionalProperties: false
4434
+ },
4239
4435
  "x-clack-sections": {
4240
4436
  type: "array",
4241
4437
  items: {
@@ -4507,6 +4703,44 @@ var authoring_form_v1_schema_default = {
4507
4703
  ],
4508
4704
  additionalProperties: false
4509
4705
  },
4706
+ "x-clack-help-i18n": {
4707
+ type: "object",
4708
+ properties: {
4709
+ ko: {
4710
+ type: "string",
4711
+ minLength: 1,
4712
+ maxLength: 1e3
4713
+ },
4714
+ en: {
4715
+ type: "string",
4716
+ minLength: 1,
4717
+ maxLength: 1e3
4718
+ }
4719
+ },
4720
+ required: [
4721
+ "ko"
4722
+ ],
4723
+ additionalProperties: false
4724
+ },
4725
+ "x-clack-placeholder-i18n": {
4726
+ type: "object",
4727
+ properties: {
4728
+ ko: {
4729
+ type: "string",
4730
+ minLength: 1,
4731
+ maxLength: 300
4732
+ },
4733
+ en: {
4734
+ type: "string",
4735
+ minLength: 1,
4736
+ maxLength: 300
4737
+ }
4738
+ },
4739
+ required: [
4740
+ "ko"
4741
+ ],
4742
+ additionalProperties: false
4743
+ },
4510
4744
  "x-clack-source": {
4511
4745
  type: "object",
4512
4746
  additionalProperties: false,
@@ -5558,7 +5792,7 @@ function parsePat(schema, input, label) {
5558
5792
  return parsed.data;
5559
5793
  }
5560
5794
  var toolDefinitions = [
5561
- { 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 } },
5795
+ { 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 } },
5562
5796
  {
5563
5797
  name: "platform_usage_get",
5564
5798
  description: "\uC11C\uBC84 \uD0A4\uAC00 \uC18D\uD55C \uCF58\uD150\uCE20\uC758 \uD50C\uB7AB\uD3FC \uC0AC\uC6A9\uB7C9\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4.",
@@ -5750,8 +5984,14 @@ var PlatformMcpServer = class {
5750
5984
  if (patDestructiveToolNames.has(name2) && (!isObject3(args) || args.confirm !== true)) {
5751
5985
  return { preview: true, confirmation_required: true, tool: name2, arguments: redact(args) };
5752
5986
  }
5987
+ if (name2 === "platform_authoring") {
5988
+ const input = parsePat(authoringInput, args, "\uC81C\uC791 \uC791\uC5C5 \uC785\uB825");
5989
+ if (input.action === "abandon" && input.confirm !== true) {
5990
+ return { preview: true, confirmation_required: true, tool: name2, arguments: redact(input) };
5991
+ }
5992
+ return authoringRequest(platformPatClient(this.settings, this.env), input, "mcp");
5993
+ }
5753
5994
  const client = platformPatClient(this.settings, this.env);
5754
- if (name2 === "platform_authoring") return authoringRequest(client, args, "mcp");
5755
5995
  if (name2 === "platform_skill_list") {
5756
5996
  const { q } = parsePat(patSchemas.skillList, args, "\uC2A4\uD0AC \uBAA9\uB85D \uC870\uD68C \uC785\uB825");
5757
5997
  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.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "클랙 계정의 상품·콘텐츠·프로필을 관리하는 CLI",
5
5
  "type": "module",
6
6
  "bin": {