@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.
- package/README.md +3 -1
- package/dist/cli.js +153 -23
- 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,
|
|
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.
|
|
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
|
-
|
|
624
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5318
|
+
const extracted = {};
|
|
5192
5319
|
try {
|
|
5193
|
-
|
|
5194
|
-
|
|
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)
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
error2.
|
|
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\
|
|
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 } });
|