@clack-platform/cli 0.1.0 → 0.1.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 +44 -10
  2. package/dist/cli.js +224 -84
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,7 +19,7 @@ clack whoami --json
19
19
  clack logout
20
20
  ```
21
21
 
22
- `login`이 표시한 코드를 앱 **마이페이지 → 계정 → 내 정보 수정하기 → 외부 도구 연결 → 코드로 승인**에서 입력한다. 요청한 권한을 확인하고 승인하면 CLI에 연결된다. 기본 요청 권한은 `profile:read` 하나다. `profile`, `product`, `content`, `creator-content`, `skill`, `platform` 묶음은 각각 읽기·쓰기를 모두 요청한다(`creator-content:publish`, `skill:publish`는 별도 명시가 필요하다). `platform`은 내 콘텐츠의 서버 키·공유 문서 관리 전용 권한이다(아래 "플랫폼 전용 PAT 도구" 절 참고). 소셜 로그인 사용자도 앱에서 승인할 수 있다. 웹 승인 화면은 별도 후속 단계이므로 현재는 앱 승인을 사용한다.
22
+ `login`이 표시한 코드를 앱 **마이페이지 → 계정 → 내 정보 수정하기 → 외부 도구 연결 → 코드로 승인**에서 입력한다. 요청한 권한을 확인하고 승인하면 CLI에 연결된다. 기본 요청 권한은 `profile:read` 하나다. `profile`, `product`, `content`, `creator-content`, `skill`, `platform`, `custom-page` 묶음은 각각 읽기·쓰기를 모두 요청한다(`creator-content:publish`, `skill:publish`, `custom-page:publish`는 별도 명시가 필요하다). 명령별 필요 권한은 각 명령의 `--help`와 아래 절의 권한 표에 적혀 있다. `platform`은 내 콘텐츠의 서버 키·공유 문서 관리 전용 권한이다(아래 "플랫폼 전용 PAT 도구" 절 참고). 소셜 로그인 사용자도 앱에서 승인할 수 있다. 웹 승인 화면은 별도 후속 단계이므로 현재는 앱 승인을 사용한다.
23
23
 
24
24
  수동 발급한 토큰은 `clack login --token`의 숨김 프롬프트에 붙여 넣는다. 비대화형 실행에서는 이 명령의 표준 입력으로 전달할 수 있다. `--token <값>`도 지원하지만 셸 기록을 피하려면 프롬프트나 `CLACK_TOKEN` 환경변수를 사용한다. 토큰 원문은 정상 결과·오류·상세 로그에 출력하지 않는다.
25
25
 
@@ -31,6 +31,22 @@ clack token list --json
31
31
  clack token revoke 123 --yes
32
32
  ```
33
33
 
34
+ ### 승인 대기 중 실행이 끊기는 경우(에이전트용)
35
+
36
+ `login`은 코드를 발급한 즉시(폴링을 시작하기 전에) 그 요청을 자격 저장소 옆에 0600 권한으로 저장해 둔다. 그래서 승인을 기다리는 도중에 프로세스가 끝나도(에이전트가 턴을 종료하는 경우 등) 승인 자체는 유실되지 않는다 — 다음 실행에서 `clack login --resume`으로 같은 코드를 이어서 기다리면 된다.
37
+
38
+ ```sh
39
+ clack login --no-wait --json # 코드만 발급하고 즉시 반환 (user_code·주소·만료 시각을 JSON으로 출력)
40
+ # … 사용자가 앱에서 승인할 시간을 준다(턴 종료 등) …
41
+ clack login --resume # 같은 요청을 이어서 기다리다가 승인되면 저장한다
42
+ ```
43
+
44
+ `--no-wait` 없이 그냥 `clack login`을 실행했다가 대기 중 중단된 경우에도 `clack login --resume`으로 이어받을 수 있다. `--resume`은 새 코드를 발급하지 않으며, 저장된 요청이 이미 만료·거부·소비됐으면(`EXPIRED_TOKEN`·`ACCESS_DENIED`·`DEVICE_CODE_CONSUMED`) 그 사실을 분명한 오류 코드로 알리고 자동으로 새 요청을 만들지 않는다 — 이때는 `clack login`을 다시 실행한다. 저장된 요청이 없으면 `DEVICE_REQUEST_NOT_FOUND`, 발급 당시와 다른 `--env`·`--base-url`로 재개를 시도하면 `DEVICE_REQUEST_ORIGIN_MISMATCH`를 반환한다(둘 다 요청을 지우지 않으므로 `--env`를 바로잡아 다시 시도할 수 있다). `--resume`은 `--token`과 함께 쓸 수 없다.
45
+
46
+ ```sh
47
+ clack login --resume --env dev # 발급 당시와 같은 환경이어야 한다. 다르면 DEVICE_REQUEST_ORIGIN_MISMATCH
48
+ ```
49
+
34
50
  ## 주요 명령
35
51
 
36
52
  세부 필드·선택지는 각 명령의 `--help`에서 확인한다.
@@ -89,7 +105,7 @@ clack channel post create --from-markdown draft.md --status draft
89
105
 
90
106
  CLI 안내는 한국어다. `lang`은 API 콘텐츠 언어에 사용한다. `--verbose`는 메서드·경로·시간 계약만 표준 오류로 표시한다. `doctor`는 현재 연결·권한·만료와 서버 메타 API의 한도·읽기/쓰기·MCP·디바이스 연결 상태를 검사한다. 메타 조회 실패는 오류로 반환한다.
91
107
 
92
- `--json` 성공 결과는 `{ok:true,data,pagination?,time_contract}`, 실패 결과는 `{ok:false,status,code,message,retry_after?}`다. 시간 문자열은 API 원문과 `time_contract`를 함께 유지한다. 사람용 출력은 알려진 시각 필드에 중앙 파서를 적용하며 `--time utc`로 실제 UTC를 표시할 수 있다. 생년월일과 본문은 시간으로 변환하지 않는다.
108
+ `--json` 성공 결과는 `{ok:true,data,pagination?,time_contract}`, 실패 결과는 `{ok:false,status,code,message,retry_after?}`다. 시간 문자열은 API 원문과 `time_contract`를 함께 유지한다. 사람용 출력은 알려진 시각 필드에 중앙 파서를 적용하며 `--time utc`로 실제 UTC를 표시할 수 있다. 생년월일과 본문은 시간으로 변환하지 않는다. 기본값(`--time local`)은 실행 환경의 로캘·타임존과 무관하게 `YYYY-MM-DD HH:MM KST` 고정 형식으로 표시한다(클랙 서비스는 항상 한국 시간 기준이다).
93
109
 
94
110
  | 종료 코드 | 의미 |
95
111
  |---|---|
@@ -124,7 +140,7 @@ clack skills --json
124
140
 
125
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`를 사용한다.
126
142
 
127
- 에이전트 스킬은 별도 공개 저장소 [clack-project/skills](https://github.com/clack-project/skills)에서 `npx skills add clack-project/skills`로 설치한다. 설치·상품·콘텐츠·채널·프로필·MCP 6종을 제공하며 `clack skills`는 안내만 출력한다. 스킬만 선택 설치할 때는 `-s clack-products`, 전역 설치에는 `-g`를 사용한다.
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`를 사용한다.
128
144
 
129
145
  ## 검증과 배포
130
146
 
@@ -140,7 +156,17 @@ GitHub Actions는 Linux·macOS·Windows × Node 20·22에서 `pnpm check`와 패
140
156
 
141
157
  ## HTML 콘텐츠 개발 테스트
142
158
 
143
- 크리에이터 콘텐츠는 기존 게시글·포켓과 별도 권한을 사용합니다. 개발 환경에서 초안 등록·HTML/ZIP 업로드를 검증할 수 있습니다. 앱 확인·AI 심사 후 공개·채팅 제작은 아직 준비 중이며 CLI로 확인 요건을 우회할 수 없습니다.
159
+ 크리에이터 콘텐츠는 기존 게시글·포켓과 별도 권한을 사용합니다. 앱 확인·심사·공개 기능의 제공 여부는 환경마다 다르며 `clack content config`의 `app_preview_enabled`·`review_enabled`·`publication_enabled`로 확인합니다. CLI로 앱 확인 요건을 우회할 수 없습니다.
160
+
161
+ | 명령 | 필요한 PAT 권한 |
162
+ |---|---|
163
+ | `config` | 없음(로그인 불필요) |
164
+ | `list`, `status`, `preview` | `creator-content:read` |
165
+ | `create`, `upload`, `complete`, `withdraw` | `creator-content:write` |
166
+ | `submit` | `creator-content:write`와 `creator-content:publish` 모두 |
167
+ | `publish`, `unpublish` | `creator-content:publish` |
168
+
169
+ 제출 순서는 `upload` → `preview`(같은 계정의 앱에서 열어 확인 완료) → `submit`입니다. 앱 확인 전에 제출하면 `PREVIEW_CONFIRMATION_REQUIRED`(409)가 반환됩니다.
144
170
 
145
171
  ```bash
146
172
  clack login --env dev --scopes creator-content:read,creator-content:write,creator-content:publish
@@ -151,7 +177,7 @@ clack content status --env dev <콘텐츠-UUID>
151
177
  clack content preview --env dev <콘텐츠-UUID> <버전-UUID>
152
178
  ```
153
179
 
154
- `content upload`는 최대 30 MiB의 HTML/ZIP을 체크섬과 함께 비공개 저장소에 전송합니다. 전송 후 완료 응답만 실패하면 출력된 `content complete` 명령으로 같은 업로드를 재처리할 수 있습니다. `--dry-run`은 파일·입력 검사만 수행합니다. `creator-content` 약식 scope는 읽기·쓰기만 요청하며 공개 권한은 명시적으로 요청해야 합니다. 정책 내용을 크리에이터 센터에서 확인한 뒤 `--policy-version`으로 동의한 버전을 지정하세요.
180
+ `content upload`는 최대 30 MiB의 HTML/ZIP을 체크섬과 함께 비공개 저장소에 전송합니다. 전송 후 완료 응답만 실패하면 출력된 `content complete` 명령으로 같은 업로드를 재처리할 수 있습니다. `--dry-run`은 파일·입력 검사만 수행합니다. `creator-content` 약식 scope는 읽기·쓰기만 요청하며 `submit`·`publish`·`unpublish`에 필요한 `creator-content:publish`는 명시적으로 요청해야 합니다. 정책 내용을 크리에이터 센터에서 확인한 뒤 `--policy-version`으로 동의한 버전을 지정하세요.
155
181
 
156
182
  ZIP에 `clack.content.json` 또는 `.clack/`가 있으면 업로드 전에 선언·경로·크기와 프로필 파일 대응을 확인하고 표준 오류에 경고합니다. 선언이 없는 기존 정적 ZIP도 계속 업로드할 수 있습니다. 경고는 업로드를 막지 않으며 서버의 최종 검증·심사 결과를 대신하지 않습니다. 전송 없이 확인하려면 `content upload ... --dry-run`을 사용하세요.
157
183
 
@@ -163,7 +189,7 @@ clack content publish --env dev <콘텐츠-UUID> <버전-UUID>
163
189
 
164
190
  ## 스킬 패키지 개발 검증
165
191
 
166
- `clack.skill.json`과 `SKILL.md`가 있는 디렉터리 또는 ZIP을 검사합니다. 로컬 검증은 로그인 없이 실행할 수 있습니다. 서버 경로는 현재 개발 환경에서 기능 플래그가 켜진 계정과 `skill:write` 권한의 개인 액세스 토큰이 필요합니다.
192
+ `clack.skill.json`과 `SKILL.md`가 있는 디렉터리 또는 ZIP을 검사합니다. 로컬 검증은 로그인 없이 실행할 수 있습니다. 서버 경로(`validate --remote`, `push` 등)는 현재 개발 환경에서 기능 플래그가 켜진 계정과 아래 표의 권한을 가진 개인 액세스 토큰이 필요합니다.
167
193
 
168
194
  ```sh
169
195
  clack skill validate ./my-skill
@@ -184,7 +210,15 @@ clack skill deprecate <스킬-UUID> --env dev
184
210
 
185
211
  `push`는 서버 사전 검증 후 새 스킬 또는 기존 스킬의 버전을 예약하고, 서명된 URL에 ZIP을 전송한 뒤 완료 처리합니다. 완료 응답만 실패하면 출력된 `skill complete` 명령으로 같은 버전을 재처리합니다. `--dry-run`은 서버를 변경하지 않습니다.
186
212
 
187
- `list`·`get`·`form`·`status`는 `skill:read`, `cancel`·`deprecate`는 `skill:write`, `submit`은 `skill:write`와 `skill:publish`, `release`는 `skill:publish` 권한이 필요합니다. 변경 명령은 개인 액세스 토큰(PAT)만 사용합니다. `submit`은 서버에서 검증 완료 상태와 패키지 해시를 조회한 뒤 확인을 받고 심사를 요청합니다. `--dry-run`은 제출 요청 없이 상태만 확인합니다. `release`는 심사를 통과한 버전만 허용하고, `--visibility public`은 별도 라이브러리 등재 승인이 필요합니다. 심사 판정은 관리자 화면에서 처리합니다. `deprecate`는 스킬 전체를 지원 종료 상태로 바꾸며 되돌릴 수 없습니다. 확인 후에만 실행되고(비대화형은 `--yes` 필요), 새 설치·새 제작은 막히지만 기존 콘텐츠가 고정한 버전은 계속 동작합니다.
213
+ | 명령 | 필요한 PAT 권한 |
214
+ |---|---|
215
+ | `validate`(로컬) | 없음(로그인 불필요) |
216
+ | `list`, `get`, `form`, `status` | `skill:read` |
217
+ | `validate --remote`, `push`, `complete`, `cancel` | `skill:write` |
218
+ | `submit` | `skill:write`와 `skill:publish` 모두 |
219
+ | `release`, `deprecate` | `skill:publish` |
220
+
221
+ `clack login --scopes skill` 약식은 읽기·쓰기만 요청하므로 제출·게시·지원 종료에는 `skill:publish`를 추가해야 합니다. 변경 명령은 개인 액세스 토큰(PAT)만 사용합니다. `submit`은 서버에서 검증 완료 상태와 패키지 해시를 조회한 뒤 확인을 받고 심사를 요청합니다. `--dry-run`은 제출 요청 없이 상태만 확인합니다. `release`는 심사를 통과한 버전만 허용하고, `--visibility public`은 별도 라이브러리 등재 승인이 필요합니다. 심사 판정은 관리자 화면에서 처리합니다. `deprecate`는 스킬 전체를 지원 종료 상태로 바꾸며 되돌릴 수 없습니다. 확인 후에만 실행되고(비대화형은 `--yes` 필요), 새 설치·새 제작은 막히지만 기존 콘텐츠가 고정한 버전은 계속 동작합니다.
188
222
 
189
223
  ### 스킬 플러그인 정적 검사(plugin-static-v2)
190
224
 
@@ -236,14 +270,14 @@ clack platform data patch scores slot --file ./score-patch.json --if-rev 1 --env
236
270
  clack platform data delete scores slot --env dev
237
271
  ```
238
272
 
239
- `clack mcp config --platform --env dev`는 서버 키 조회와 PAT 개인 도구를 함께 쓸 수 있는 로컬 stdio MCP 설정을 출력합니다. 에이전트 실행 환경에 서버 키 도구용 `CLACK_SERVER_KEY`, 스킬·콘텐츠 공개·공유 문서 도구용 `CLACK_TOKEN`(`platform:read`/`platform:write`, 필요 시 `skill:*`·`creator-content:publish`)을 각각 필요한 만큼만 설정합니다. 둘 다 로컬 프로세스 환경변수로만 전달되며 CLI 자격 파일에서 자동으로 읽지 않습니다. Claude 설정 조각은 기본 출력, Codex 설정 조각은 `--codex`로 받습니다. 설정에는 키·토큰 원문이 들어가지 않습니다.
273
+ `clack mcp config --platform --env dev`는 서버 키 조회와 PAT 개인 도구를 함께 쓸 수 있는 로컬 stdio MCP 설정을 출력합니다. 에이전트 실행 환경에 서버 키 도구용 `CLACK_SERVER_KEY`, 스킬·제작·콘텐츠 공개·공유 문서 도구용 `CLACK_TOKEN`(`platform:read`/`platform:write`, 필요 시 `skill:*`, 제작은 `creator-content:write`, 콘텐츠 공개는 `creator-content:publish`)을 각각 필요한 만큼만 설정합니다. 둘 다 로컬 프로세스 환경변수로만 전달되며 CLI 자격 파일에서 자동으로 읽지 않습니다. Claude 설정 조각은 기본 출력, Codex 설정 조각은 `--codex`로 받습니다. 설정에는 키·토큰 원문이 들어가지 않습니다.
240
274
 
241
275
  ```sh
242
276
  clack mcp config --platform --env dev
243
277
  clack mcp config --platform --codex --env dev
244
278
  ```
245
279
 
246
- 서버 키(`CLACK_SERVER_KEY`) 도구는 `platform_usage_get`, `platform_server_data_get/list/leaderboard/put/patch/delete`입니다. PAT(`CLACK_TOKEN`) 도구는 `platform_skill_list/get/push/status/submit/release`, `platform_content_publish`, `platform_shared_collections_list`, `platform_shared_documents_list`, `platform_shared_document_get/hide/delete`입니다. `platform_skill_push`는 `dir`(생략 시 현재 디렉터리)의 로컬 스킬 패키지를 검증·업로드합니다. 파괴적 도구(단일 문서 변경, 스킬 심사 제출·공개, 콘텐츠 공개, 공유 문서 숨김·삭제)는 먼저 미리보기와 `confirmation_required`를 반환하고, 입력에 `confirm: true`를 명시해야 실제 요청을 보냅니다. `mcp serve-platform`은 MCP 클라이언트가 실행하는 명령으로, 표준 출력은 JSON-RPC 메시지 전용입니다. 서버 키·PAT의 인증·스코프·테넌트 검사는 모두 API에서 수행합니다.
280
+ 서버 키(`CLACK_SERVER_KEY`) 도구는 `platform_usage_get`, `platform_server_data_get/list/leaderboard/put/patch/delete`입니다. PAT(`CLACK_TOKEN`) 도구는 `platform_authoring`, `platform_skill_list/get/push/status/submit/release`, `platform_content_publish`, `platform_shared_collections_list`, `platform_shared_documents_list`, `platform_shared_document_get/hide/delete`입니다. `platform_skill_push`는 `dir`(생략 시 현재 디렉터리)의 로컬 스킬 패키지를 검증·업로드합니다. 파괴적 도구(단일 문서 변경, 스킬 심사 제출·공개, 콘텐츠 공개, 공유 문서 숨김·삭제)는 먼저 미리보기와 `confirmation_required`를 반환하고, 입력에 `confirm: true`를 명시해야 실제 요청을 보냅니다. `mcp serve-platform`은 MCP 클라이언트가 실행하는 명령으로, 표준 출력은 JSON-RPC 메시지 전용입니다. 서버 키·PAT의 인증·스코프·테넌트 검사는 모두 API에서 수행합니다.
247
281
 
248
282
  ## 세계관·공개 홈 꾸미기
249
283
 
@@ -286,7 +320,7 @@ clack page submit PAGE_ID NEW_VERSION_ID
286
320
  clack page status PAGE_ID
287
321
  ```
288
322
 
289
- `presentation`은 재호출하면 다른 버전을 만들므로 응답이 유실되면 `status`의 버전 이력을 먼저 확인한다. 같은 업로드를 확정하는 `complete`의 멱등 재시도와 구분한다. 파일은 재사용해도 새 버전은 일일 업로드·논리 저장 한도에 포함된다. 외부 MCP에서도 `revise_custom_page_presentation`으로 같은 작업을 수행하며 상세 계약은 [HTML·페이지 제작 가이드](../clack-developer/content/guide/reference/content/upload-html.md)를 참조한다.
323
+ `presentation`은 재호출하면 다른 버전을 만들므로 응답이 유실되면 `status`의 버전 이력을 먼저 확인한다. 같은 업로드를 확정하는 `complete`의 멱등 재시도와 구분한다. 파일은 재사용해도 새 버전은 일일 업로드·논리 저장 한도에 포함된다. 외부 MCP에서도 `revise_custom_page_presentation`으로 같은 작업을 수행한다. 헤더 값과 화면 표시·안전 영역 규칙은 스킬 저장소의 [페이지 헤더·화면 표시 규칙](https://github.com/clack-project/skills/blob/main/clack-page/references/presentation.md)을 참고한다.
290
324
 
291
325
  ### 이미지 화보·캐릭터 가져오기 제작
292
326
 
package/dist/cli.js CHANGED
@@ -86,6 +86,20 @@ function requestTimeContract(method, path) {
86
86
  ].includes(path) || /^\/v4\/posts\/\d+(?:\/comments|\/merchandises)?$/.test(path) || /^\/v4\/merchandises\/\d+$/.test(path) || /^\/v4\/channels\/[^/]+(?:\/posts|\/series)?$/.test(path) || /^\/v4\/(?:channel-posts|channel-series)\/\d+$/.test(path) || /^\/v4\/search\/(?:posts|merchandises-with-post|merchandises-without-post|channel-posts|channels)$/.test(path)) return "utc-v1";
87
87
  return "legacy-kst";
88
88
  }
89
+ var KST_PARTS = new Intl.DateTimeFormat("en-CA", {
90
+ timeZone: "Asia/Seoul",
91
+ year: "numeric",
92
+ month: "2-digit",
93
+ day: "2-digit",
94
+ hour: "2-digit",
95
+ minute: "2-digit",
96
+ hour12: false
97
+ });
98
+ function formatKst(date) {
99
+ const parts = Object.fromEntries(KST_PARTS.formatToParts(date).map((part) => [part.type, part.value]));
100
+ const hour = parts.hour === "24" ? "00" : parts.hour;
101
+ return `${parts.year}-${parts.month}-${parts.day} ${hour}:${parts.minute} KST`;
102
+ }
89
103
  function formatInstant(value2, contract, mode) {
90
104
  const iso = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value2);
91
105
  const legacyWallClock = contract === "legacy-kst" && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{3})?$/.test(value2);
@@ -93,10 +107,7 @@ function formatInstant(value2, contract, mode) {
93
107
  const source = contract === "legacy-kst" ? (iso ? value2.slice(0, -1) : value2.replace(" ", "T")) + "+09:00" : value2;
94
108
  const date = new Date(source);
95
109
  if (!Number.isFinite(date.getTime())) return value2;
96
- return mode === "utc" ? date.toISOString() : new Intl.DateTimeFormat("ko-KR", {
97
- dateStyle: "medium",
98
- timeStyle: "long"
99
- }).format(date);
110
+ return mode === "utc" ? date.toISOString() : formatKst(date);
100
111
  }
101
112
  var instantKeys = /* @__PURE__ */ new Set(["created_at", "updated_at", "published_at", "hidden_at", "owner_hidden_at", "delete_available_at", "lastBulkBumpedAt", "deleted_at", "expires_at", "revoked_at", "suspended_at", "last_used_at"]);
102
113
  function displayData(value2, contract, mode) {
@@ -139,7 +150,7 @@ function redact(value2) {
139
150
  // package.json
140
151
  var package_default = {
141
152
  name: "@clack-platform/cli",
142
- version: "0.1.0",
153
+ version: "0.1.1",
143
154
  description: "\uD074\uB799 \uACC4\uC815\uC758 \uC0C1\uD488\xB7\uCF58\uD150\uCE20\xB7\uD504\uB85C\uD544\uC744 \uAD00\uB9AC\uD558\uB294 CLI",
144
155
  type: "module",
145
156
  bin: {
@@ -606,7 +617,7 @@ async function authoringRequest(client, raw, surface = "cli") {
606
617
  return client.request(action === "save" ? "PATCH" : "POST", `${path}/${action === "save" ? "form" : action}`, { body });
607
618
  }
608
619
  function registerAuthoringCommands(program2, runtime2) {
609
- 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").requiredOption("--input <file>", "action\uACFC \uC694\uCCAD \uAC12\uC774 \uC788\uB294 JSON \uD30C\uC77C"), async (ctx, _args, opts) => {
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) => {
610
621
  const input = await readPlatformBodyFile(opts.input);
611
622
  if (ctx.options.dryRun) {
612
623
  const parsed = authoringInput.safeParse(input);
@@ -667,7 +678,7 @@ var ConfigStore = class {
667
678
  try {
668
679
  const stat3 = await lstat3(path);
669
680
  if (!stat3.isFile() || stat3.isSymbolicLink()) throw new CliError("\uC124\uC815 \uD30C\uC77C\uC740 \uC77C\uBC18 \uD30C\uC77C\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.", "CONFIG_UNSAFE");
670
- if (name2 === "credentials.json" && process.platform !== "win32") await chmod(path, 384);
681
+ if ((name2 === "credentials.json" || name2 === "device-request.json") && process.platform !== "win32") await chmod(path, 384);
671
682
  const body = JSON.parse(await readFile3(path, "utf8"));
672
683
  if (!body || typeof body !== "object" || !("profiles" in body) || !body.profiles || typeof body.profiles !== "object" || Array.isArray(body.profiles)) throw new Error("\uD615\uC2DD");
673
684
  return body;
@@ -737,6 +748,21 @@ var ConfigStore = class {
737
748
  delete all.profiles[validateProfile(profile)];
738
749
  await this.write("credentials.json", all);
739
750
  }
751
+ /** 발급한 디바이스 요청을 저장해 `login`이 같은 턴에서 끝나도 `login --resume`으로 이어받을 수 있게 한다(E1). */
752
+ async savePendingDeviceRequest(profile, request) {
753
+ const all = await this.read("device-request.json");
754
+ all.profiles[validateProfile(profile)] = request;
755
+ await this.write("device-request.json", all);
756
+ }
757
+ async getPendingDeviceRequest(profile) {
758
+ return (await this.read("device-request.json")).profiles[validateProfile(profile)];
759
+ }
760
+ /** 만료·거부·소비 등으로 더 이상 재개할 수 없는 요청을 정리한다. 네트워크 오류처럼 일시적인 실패에는 호출하지 않는다. */
761
+ async removePendingDeviceRequest(profile) {
762
+ const all = await this.read("device-request.json");
763
+ delete all.profiles[validateProfile(profile)];
764
+ await this.write("device-request.json", all);
765
+ }
740
766
  async getConfig(profile) {
741
767
  return (await this.read("config.json")).profiles[validateProfile(profile)] ?? {};
742
768
  }
@@ -846,6 +872,57 @@ async function pollDevice(api, request, deps = {
846
872
  }
847
873
  throw new CliError("\uC2B9\uC778 \uCF54\uB4DC\uAC00 \uB9CC\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. clack login\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.", "EXPIRED_TOKEN", 410);
848
874
  }
875
+ var TERMINAL_DEVICE_FAILURE_CODES = ["EXPIRED_TOKEN", "ACCESS_DENIED", "DEVICE_CODE_CONSUMED", "DEVICE_AUTHORIZATION_NOT_FOUND"];
876
+ function validateVerificationUrl(href2) {
877
+ let verification;
878
+ try {
879
+ verification = new URL(href2);
880
+ } catch {
881
+ throw new CliError("\uC2B9\uC778 \uD398\uC774\uC9C0 \uC8FC\uC18C\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.", "INVALID_RESPONSE", 0);
882
+ }
883
+ if (verification.protocol !== "https:" || !["clack.kr", "dev.clack.kr", "www.clack.kr"].includes(verification.hostname) || verification.username || verification.password || verification.port) throw new CliError("\uC2B9\uC778 \uD398\uC774\uC9C0 \uC8FC\uC18C\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.", "INVALID_RESPONSE", 0);
884
+ return verification;
885
+ }
886
+ function validateFreshDeviceRequest(request) {
887
+ if (!/^dvc_[a-f0-9]{64}$/.test(request.device_code) || !/^[A-Z2-9]{4}-[A-Z2-9]{4}$/.test(request.user_code) || !Number.isFinite(request.expires_in) || request.expires_in <= 0 || request.expires_in > 600 || !Number.isFinite(request.interval) || request.interval < 1) throw new CliError("\uC2B9\uC778 \uC694\uCCAD \uC751\uB2F5\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.", "INVALID_RESPONSE", 0);
888
+ return validateVerificationUrl(request.verification_uri_complete);
889
+ }
890
+ function validatePendingRequest(pending) {
891
+ if (!/^dvc_[a-f0-9]{64}$/.test(pending.device_code) || !/^[A-Z2-9]{4}-[A-Z2-9]{4}$/.test(pending.user_code) || !Number.isFinite(pending.interval) || pending.interval < 1 || !Number.isFinite(Date.parse(pending.expires_at))) {
892
+ throw new CliError("\uC800\uC7A5\uB41C \uB85C\uADF8\uC778 \uC694\uCCAD \uC815\uBCF4\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. clack login\uC73C\uB85C \uC0C8\uB85C \uC2DC\uC791\uD558\uC138\uC694.", "CONFIG_UNSAFE");
893
+ }
894
+ return validateVerificationUrl(pending.verification_uri_complete);
895
+ }
896
+ function newPendingRequest(request, scopes, baseUrl) {
897
+ return {
898
+ device_code: request.device_code,
899
+ user_code: request.user_code,
900
+ verification_uri: request.verification_uri,
901
+ verification_uri_complete: request.verification_uri_complete,
902
+ interval: request.interval,
903
+ scopes,
904
+ base_url: baseUrl,
905
+ expires_at: new Date(Date.now() + request.expires_in * 1e3).toISOString(),
906
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
907
+ };
908
+ }
909
+ function remainingDeviceRequest(pending) {
910
+ return {
911
+ device_code: pending.device_code,
912
+ user_code: pending.user_code,
913
+ verification_uri: pending.verification_uri,
914
+ verification_uri_complete: pending.verification_uri_complete,
915
+ interval: pending.interval,
916
+ expires_in: Math.max(0, Math.floor((Date.parse(pending.expires_at) - Date.now()) / 1e3))
917
+ };
918
+ }
919
+ async function announceDeviceCode(verification, userCode, opts, json2) {
920
+ process.stderr.write(redactText(`\uC571 \uB9C8\uC774\uD398\uC774\uC9C0 \u2192 \uACC4\uC815 \u2192 \uB0B4 \uC815\uBCF4 \uC218\uC815\uD558\uAE30 \u2192 \uC678\uBD80 \uB3C4\uAD6C \uC5F0\uACB0 \u2192 \uCF54\uB4DC\uB85C \uC2B9\uC778\uC5D0\uC11C ${userCode} \uC785\uB825
921
+ ${verification.href}
922
+ `));
923
+ if (opts.qr !== false && process.stderr.isTTY && !json2) qr.generate(verification.href, { small: true }, (value2) => process.stderr.write(value2 + "\n"));
924
+ if (opts.browser && process.stdin.isTTY && !json2) await open2(verification.href).catch(() => process.stderr.write("\uBE0C\uB77C\uC6B0\uC800\uB97C \uC5F4\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uC704 \uC8FC\uC18C\uB098 \uC571\uC5D0\uC11C \uC2B9\uC778\uD558\uC138\uC694.\n"));
925
+ }
849
926
  async function tokenInput(option) {
850
927
  if (typeof option === "string") return validateToken(option);
851
928
  if (process.env.CLACK_TOKEN) return validateToken(process.env.CLACK_TOKEN);
@@ -858,11 +935,12 @@ async function tokenInput(option) {
858
935
  return validateToken(value2);
859
936
  }
860
937
  function registerAuthCommands(program2, runtime2, store2) {
861
- runtime2.action(program2.command("login").description("\uC571\uC5D0\uC11C \uC2B9\uC778\uD558\uAC70\uB098 \uAC1C\uC778 \uC561\uC138\uC2A4 \uD1A0\uD070\uC73C\uB85C \uC5F0\uACB0").option("--token [pat]", "\uD1A0\uD070 \uC785\uB825. \uAC12\uC744 \uC0DD\uB7B5\uD558\uBA74 \uC228\uAE40 \uD504\uB86C\uD504\uD2B8 \uB610\uB294 \uD45C\uC900 \uC785\uB825 \uC0AC\uC6A9").option("--scopes <scopes>", "\uC694\uCCAD \uAD8C\uD55C (\uC27C\uD45C \uAD6C\uBD84)", "profile:read").option("--no-browser", "\uBE0C\uB77C\uC6B0\uC800 \uC790\uB3D9 \uC5F4\uAE30 \uC0DD\uB7B5").option("--no-qr", "QR \uCF54\uB4DC \uCD9C\uB825 \uC0DD\uB7B5 (\uC5D0\uC774\uC804\uD2B8\xB7\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC5D0 \uC801\uD569)").option("--label <name>", "\uAE30\uAE30 \uC774\uB984", hostname()), async (ctx, _args, opts) => {
938
+ runtime2.action(program2.command("login").description("\uC571\uC5D0\uC11C \uC2B9\uC778\uD558\uAC70\uB098 \uAC1C\uC778 \uC561\uC138\uC2A4 \uD1A0\uD070\uC73C\uB85C \uC5F0\uACB0").option("--token [pat]", "\uD1A0\uD070 \uC785\uB825. \uAC12\uC744 \uC0DD\uB7B5\uD558\uBA74 \uC228\uAE40 \uD504\uB86C\uD504\uD2B8 \uB610\uB294 \uD45C\uC900 \uC785\uB825 \uC0AC\uC6A9").option("--scopes <scopes>", "\uC694\uCCAD \uAD8C\uD55C (\uC27C\uD45C \uAD6C\uBD84). product \uAC19\uC740 \uBB36\uC74C \uC774\uB984\uC740 :read\xB7:write\uB9CC \uC694\uCCAD\uD558\uBA70, \uC81C\uCD9C\xB7\uAC8C\uC2DC\uC5D0 \uD544\uC694\uD55C *:publish\uB294 \uB530\uB85C \uC801\uB294\uB2E4", "profile:read").option("--no-browser", "\uBE0C\uB77C\uC6B0\uC800 \uC790\uB3D9 \uC5F4\uAE30 \uC0DD\uB7B5").option("--no-qr", "QR \uCF54\uB4DC \uCD9C\uB825 \uC0DD\uB7B5 (\uC5D0\uC774\uC804\uD2B8\xB7\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC5D0 \uC801\uD569)").option("--label <name>", "\uAE30\uAE30 \uC774\uB984", hostname()).option("--no-wait", "\uCF54\uB4DC\uB9CC \uBC1C\uAE09\uD558\uACE0 \uC2B9\uC778\uC744 \uAE30\uB2E4\uB9AC\uC9C0 \uC54A\uC74C (\uAC19\uC740 \uD134\uC744 \uB05D\uB0B4\uC57C \uD558\uB294 \uC5D0\uC774\uC804\uD2B8\uC5D0 \uC801\uD569, \uC774\uD6C4 clack login --resume\uC73C\uB85C \uC774\uC5B4\uBC1B\uC74C)").option("--resume", "\uC774\uC804\uC5D0 \uBC1C\uAE09\uD588\uC9C0\uB9CC \uC544\uC9C1 \uB9CC\uB8CC\uB418\uC9C0 \uC54A\uC740 \uC694\uCCAD\uC744 \uC774\uC5B4\uC11C \uAE30\uB2E4\uB9BC (\uC0C8 \uCF54\uB4DC\uB97C \uBC1C\uAE09\uD558\uC9C0 \uC54A\uC74C)"), async (ctx, _args, opts) => {
862
939
  if (ctx.options.dryRun) throw new CliError("login\uC740 --dry-run\uC744 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
863
940
  const resolved = await store2.resolve(ctx.options, true);
864
941
  let credential;
865
942
  if (opts.token !== void 0 || process.env.CLACK_TOKEN) {
943
+ if (opts.resume) throw new CliError("--resume\uC740 --token\uACFC \uD568\uAED8 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
866
944
  const token = await tokenInput(opts.token);
867
945
  const api = new ApiClient({ baseUrl: resolved.baseUrl, token, verbose: ctx.options.verbose });
868
946
  const row = await currentToken(api, token);
@@ -873,36 +951,77 @@ function registerAuthCommands(program2, runtime2, store2) {
873
951
  credential.user = { id: result.data.id, name: result.data.name, avatar: result.data.avatar };
874
952
  }
875
953
  } else {
876
- const scopes = parseScopes(opts.scopes);
877
- if (typeof opts.label !== "string" || !opts.label.trim() || opts.label.length > 120) throw new CliError("\uAE30\uAE30 \uC774\uB984\uC740 1~120\uC790\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
878
- const { data: request } = await ctx.api.request("POST", "/v4/auth/device/code", { auth: false, body: {
879
- client_name: "cli",
880
- client_version: VERSION,
881
- device_name: opts.label,
882
- scopes
883
- } });
884
- rememberSecret(request.device_code);
885
- if (!/^dvc_[a-f0-9]{64}$/.test(request.device_code) || !/^[A-Z2-9]{4}-[A-Z2-9]{4}$/.test(request.user_code) || !Number.isFinite(request.expires_in) || request.expires_in <= 0 || request.expires_in > 600 || !Number.isFinite(request.interval) || request.interval < 1) throw new CliError("\uC2B9\uC778 \uC694\uCCAD \uC751\uB2F5\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.", "INVALID_RESPONSE", 0);
886
- const verification = new URL(request.verification_uri_complete);
887
- if (verification.protocol !== "https:" || !["clack.kr", "dev.clack.kr", "www.clack.kr"].includes(verification.hostname) || verification.username || verification.password || verification.port) throw new CliError("\uC2B9\uC778 \uD398\uC774\uC9C0 \uC8FC\uC18C\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.", "INVALID_RESPONSE", 0);
888
- process.stderr.write(redactText(`\uC571 \uB9C8\uC774\uD398\uC774\uC9C0 \u2192 \uACC4\uC815 \u2192 \uB0B4 \uC815\uBCF4 \uC218\uC815\uD558\uAE30 \u2192 \uC678\uBD80 \uB3C4\uAD6C \uC5F0\uACB0 \u2192 \uCF54\uB4DC\uB85C \uC2B9\uC778\uC5D0\uC11C ${request.user_code} \uC785\uB825
889
- ${verification.href}
890
- `));
891
- if (opts.qr !== false && process.stderr.isTTY && !ctx.options.json) qr.generate(verification.href, { small: true }, (value2) => process.stderr.write(value2 + "\n"));
892
- if (opts.browser && process.stdin.isTTY && !ctx.options.json) await open2(verification.href).catch(() => process.stderr.write("\uBE0C\uB77C\uC6B0\uC800\uB97C \uC5F4\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uC704 \uC8FC\uC18C\uB098 \uC571\uC5D0\uC11C \uC2B9\uC778\uD558\uC138\uC694.\n"));
893
- const grant = await pollDevice(ctx.api, request);
954
+ let pending;
955
+ if (opts.resume) {
956
+ const stored = await store2.getPendingDeviceRequest(resolved.profile);
957
+ if (!stored) throw new CliError("\uB300\uAE30 \uC911\uC778 \uB85C\uADF8\uC778 \uC694\uCCAD\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. clack login\uC73C\uB85C \uC0C8\uB85C \uC2DC\uC791\uD558\uC138\uC694.", "DEVICE_REQUEST_NOT_FOUND", 404);
958
+ if ((ctx.options.env || ctx.options.baseUrl) && resolved.baseUrl !== stored.base_url) {
959
+ throw new CliError(`\uB300\uAE30 \uC911\uC778 \uC694\uCCAD\uC740 ${stored.base_url}\uC5D0 \uBC1C\uAE09\uB418\uC5C8\uC2B5\uB2C8\uB2E4. --env\xB7--base-url \uC5C6\uC774 clack login --resume\uC744 \uC2E4\uD589\uD558\uC138\uC694.`, "DEVICE_REQUEST_ORIGIN_MISMATCH", 401);
960
+ }
961
+ pending = stored;
962
+ } else {
963
+ const scopes = parseScopes(opts.scopes);
964
+ if (typeof opts.label !== "string" || !opts.label.trim() || opts.label.length > 120) throw new CliError("\uAE30\uAE30 \uC774\uB984\uC740 1~120\uC790\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
965
+ const { data: request } = await ctx.api.request("POST", "/v4/auth/device/code", { auth: false, body: {
966
+ client_name: "cli",
967
+ client_version: VERSION,
968
+ device_name: opts.label,
969
+ scopes
970
+ } });
971
+ rememberSecret(request.device_code);
972
+ validateFreshDeviceRequest(request);
973
+ pending = newPendingRequest(request, scopes, resolved.baseUrl);
974
+ await store2.savePendingDeviceRequest(resolved.profile, pending);
975
+ }
976
+ rememberSecret(pending.device_code);
977
+ const verification = validatePendingRequest(pending);
978
+ await announceDeviceCode(verification, pending.user_code, opts, Boolean(ctx.options.json));
979
+ if (opts.wait === false) {
980
+ const expiry = formatInstant(pending.expires_at, "utc-v1", ctx.options.time ?? "local");
981
+ if (ctx.options.json) {
982
+ ctx.output({
983
+ status: "pending",
984
+ user_code: pending.user_code,
985
+ verification_uri: pending.verification_uri,
986
+ verification_uri_complete: pending.verification_uri_complete,
987
+ expires_at: pending.expires_at,
988
+ interval: pending.interval,
989
+ message: "\uC571\uC5D0\uC11C \uC2B9\uC778\uD55C \uB4A4 \uC774\uC5B4\uC11C \uBC1B\uC73C\uB824\uBA74 clack login --resume\uC744 \uC2E4\uD589\uD558\uC138\uC694."
990
+ });
991
+ } else {
992
+ process.stdout.write(redactText([
993
+ `\uC2B9\uC778\uC744 \uAE30\uB2E4\uB9AC\uC9C0 \uC54A\uACE0 \uBC18\uD658\uD569\uB2C8\uB2E4. (\uD504\uB85C\uD544: ${resolved.profile}, \uC8FC\uC18C: ${pending.base_url})`,
994
+ `\uCF54\uB4DC\uB294 ${expiry}\uAE4C\uC9C0 \uC720\uD6A8\uD569\uB2C8\uB2E4.`,
995
+ "\uC571\uC5D0\uC11C \uC2B9\uC778\uD55C \uB4A4 \uC774\uC5B4\uC11C \uBC1B\uC73C\uB824\uBA74: clack login --resume"
996
+ ].join("\n") + "\n"));
997
+ }
998
+ return;
999
+ }
1000
+ const api = opts.resume ? new ApiClient({ baseUrl: pending.base_url, verbose: ctx.options.verbose }) : ctx.api;
1001
+ let grant;
1002
+ try {
1003
+ grant = await pollDevice(api, remainingDeviceRequest(pending));
1004
+ } catch (error2) {
1005
+ if (error2 instanceof CliError && TERMINAL_DEVICE_FAILURE_CODES.includes(error2.code)) {
1006
+ await store2.removePendingDeviceRequest(resolved.profile).catch(() => {
1007
+ });
1008
+ }
1009
+ throw error2;
1010
+ }
894
1011
  const token = validateToken(grant.token);
895
- if (!Array.isArray(grant.scopes) || grant.scopes.some((scope) => !scopes.includes(scope)) || !Number.isFinite(Date.parse(grant.expires_at)) || !grant.user || !Number.isSafeInteger(grant.user.id)) throw new CliError("\uAD50\uBD80 \uC751\uB2F5\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC571\uC5D0\uC11C \uC5F0\uACB0 \uBAA9\uB85D\uC744 \uD655\uC778\uD558\uC138\uC694.", "INVALID_RESPONSE", 0);
896
- credential = { token, base_url: resolved.baseUrl, scopes: grant.scopes, expires_at: grant.expires_at, user: grant.user };
1012
+ if (!Array.isArray(grant.scopes) || grant.scopes.some((scope) => !pending.scopes.includes(scope)) || !Number.isFinite(Date.parse(grant.expires_at)) || !grant.user || !Number.isSafeInteger(grant.user.id)) throw new CliError("\uAD50\uBD80 \uC751\uB2F5\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC571\uC5D0\uC11C \uC5F0\uACB0 \uBAA9\uB85D\uC744 \uD655\uC778\uD558\uC138\uC694.", "INVALID_RESPONSE", 0);
1013
+ credential = { token, base_url: pending.base_url, scopes: grant.scopes, expires_at: grant.expires_at, user: grant.user };
1014
+ await store2.removePendingDeviceRequest(resolved.profile).catch(() => {
1015
+ });
897
1016
  }
898
1017
  await store2.saveCredential(resolved.profile, credential);
899
- await store2.setConfig(resolved.profile, "base_url", resolved.baseUrl);
1018
+ await store2.setConfig(resolved.profile, "base_url", credential.base_url);
900
1019
  if (ctx.options.json) {
901
1020
  ctx.output({ profile: resolved.profile, user: credential.user ?? null, scopes: credential.scopes, expires_at: credential.expires_at, message: "\uC5F0\uACB0\uB418\uC5C8\uC2B5\uB2C8\uB2E4." });
902
1021
  } else {
903
1022
  const expiry = formatInstant(credential.expires_at, "utc-v1", ctx.options.time ?? "local");
904
1023
  process.stdout.write(redactText([
905
- `\uC5F0\uACB0\uB418\uC5C8\uC2B5\uB2C8\uB2E4. (\uD504\uB85C\uD544: ${resolved.profile}, \uC8FC\uC18C: ${resolved.baseUrl})`,
1024
+ `\uC5F0\uACB0\uB418\uC5C8\uC2B5\uB2C8\uB2E4. (\uD504\uB85C\uD544: ${resolved.profile}, \uC8FC\uC18C: ${credential.base_url})`,
906
1025
  credential.user ? `\uC0AC\uC6A9\uC790: ${credential.user.name ?? "\uC774\uB984 \uC5C6\uC74C"} (#${credential.user.id})` : "\uC0AC\uC6A9\uC790: profile:read \uAD8C\uD55C\uC774 \uC5C6\uC5B4 \uC870\uD68C\uD558\uC9C0 \uC54A\uC74C",
907
1026
  `\uAD8C\uD55C: ${credential.scopes.join(", ")}`,
908
1027
  `\uB9CC\uB8CC: ${expiry}`
@@ -2086,15 +2205,15 @@ var keyId = (value2) => {
2086
2205
  return id2;
2087
2206
  };
2088
2207
  function registerContentCommands(program2, runtime2) {
2089
- const content = program2.command("content").description("HTML \uCF58\uD150\uCE20 \uB4F1\uB85D\xB7\uC571 \uD655\uC778\xB7\uC2EC\uC0AC \uAD00\uB9AC");
2090
- runtime2.action(content.command("config"), async (ctx) => ctx.output(await ctx.api.request("GET", "/v4/creator/config", { auth: false })));
2091
- runtime2.action(content.command("list"), async (ctx) => ctx.output(await ctx.api.request("GET", "/v4/creator/contents")));
2092
- runtime2.action(content.command("status <id>"), async (ctx, [id2]) => ctx.output(await ctx.api.request("GET", `/v4/creator/contents/${uuid(id2)}`)));
2093
- runtime2.action(content.command("create").requiredOption("--title <text>", "\uC81C\uBAA9").option("--description <text>", "\uC124\uBA85", "").option("--kind <kind>", "html|gallery|slideshow|video", "html").requiredOption("--policy-version <version>", "\uB3D9\uC758\uD55C \uCF58\uD150\uCE20 \uC81C\uACF5 \uC815\uCC45 \uBC84\uC804"), async (ctx, _args, opts) => {
2208
+ const content = program2.command("content").description("HTML \uCF58\uD150\uCE20 \uB4F1\uB85D\xB7\uC571 \uD655\uC778\xB7\uC2EC\uC0AC \uAD00\uB9AC (creator-content:read\xB7write\xB7publish)");
2209
+ runtime2.action(content.command("config").description("\uCF58\uD150\uCE20 \uAE30\uB2A5 \uC81C\uACF5 \uC5EC\uBD80\xB7\uC124\uC815 \uC870\uD68C (\uB85C\uADF8\uC778 \uBD88\uD544\uC694)"), async (ctx) => ctx.output(await ctx.api.request("GET", "/v4/creator/config", { auth: false })));
2210
+ runtime2.action(content.command("list").description("\uB0B4 \uCF58\uD150\uCE20 \uBAA9\uB85D (creator-content:read)"), async (ctx) => ctx.output(await ctx.api.request("GET", "/v4/creator/contents")));
2211
+ runtime2.action(content.command("status <id>").description("\uCF58\uD150\uCE20\uC640 \uBC84\uC804\uBCC4 \uC5C5\uB85C\uB4DC\xB7\uC2EC\uC0AC\xB7\uACF5\uAC1C \uC0C1\uD0DC (creator-content:read)"), async (ctx, [id2]) => ctx.output(await ctx.api.request("GET", `/v4/creator/contents/${uuid(id2)}`)));
2212
+ runtime2.action(content.command("create").description("\uCF58\uD150\uCE20 \uCD08\uC548 \uB4F1\uB85D (creator-content:write)").requiredOption("--title <text>", "\uC81C\uBAA9").option("--description <text>", "\uC124\uBA85", "").option("--kind <kind>", "html|gallery|slideshow|video", "html").requiredOption("--policy-version <version>", "\uB3D9\uC758\uD55C \uCF58\uD150\uCE20 \uC81C\uACF5 \uC815\uCC45 \uBC84\uC804"), async (ctx, _args, opts) => {
2094
2213
  const body = parse(createSchema, { title: opts.title, description: opts.description, kind: opts.kind, policy_version: opts.policyVersion });
2095
2214
  await mutate(ctx, "POST", "/v4/creator/contents", body);
2096
2215
  });
2097
- runtime2.action(content.command("upload <id> <file>").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) => {
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) => {
2098
2217
  const path = `/v4/creator/contents/${uuid(id2)}`;
2099
2218
  const prepared = await prepareContentFile(file);
2100
2219
  for (const warning of prepared.warnings) process.stderr.write(`\uD328\uD0A4\uC9C0 \uACBD\uACE0: ${warning}
@@ -2110,39 +2229,39 @@ function registerContentCommands(program2, runtime2) {
2110
2229
  throw error2;
2111
2230
  }
2112
2231
  });
2113
- runtime2.action(content.command("complete <id> <upload-id>"), async (ctx, [id2, uploadId]) => mutate(ctx, "POST", `/v4/creator/contents/${uuid(id2)}/uploads/${uuid(uploadId)}/complete`));
2114
- runtime2.action(content.command("preview <id> <version-id>"), async (ctx, [id2, versionId]) => ctx.output(await ctx.api.request("GET", `/v4/creator/contents/${uuid(id2)}/versions/${uuid(versionId)}/preview`)));
2115
- runtime2.action(content.command("submit <id> <version-id>"), 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?"));
2116
- runtime2.action(content.command("withdraw <id> <version-id>"), 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?"));
2117
- runtime2.action(content.command("publish <id> <version-id>"), 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?"));
2118
- runtime2.action(content.command("unpublish <id>"), 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?"));
2232
+ runtime2.action(content.command("complete <id> <upload-id>").description("\uC5C5\uB85C\uB4DC \uC644\uB8CC \uCC98\uB9AC \uC7AC\uC2DC\uB3C4 (creator-content:write)"), async (ctx, [id2, uploadId]) => mutate(ctx, "POST", `/v4/creator/contents/${uuid(id2)}/uploads/${uuid(uploadId)}/complete`));
2233
+ 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`)));
2234
+ 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?"));
2235
+ 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?"));
2236
+ 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?"));
2237
+ 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?"));
2119
2238
  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)");
2120
- runtime2.action(serverKeys.command("list <id>"), async (ctx, [id2]) => ctx.output(await ctx.api.request("GET", `/v4/creator/contents/${uuid(id2)}/server-keys`)));
2239
+ runtime2.action(serverKeys.command("list <id>").description("\uC11C\uBC84 \uD0A4 \uBAA9\uB85D (platform:read)"), async (ctx, [id2]) => ctx.output(await ctx.api.request("GET", `/v4/creator/contents/${uuid(id2)}/server-keys`)));
2121
2240
  for (const name2 of ["issue", "rotate"]) {
2122
2241
  runtime2.action(serverKeys.command(name2).description("\uD06C\uB9AC\uC5D0\uC774\uD130 \uC13C\uD130 \uC804\uC6A9 \uC548\uB0B4").argument("[args...]").allowUnknownOption(), async () => {
2123
2242
  throw new CliError(SERVER_KEY_CENTER_ONLY, "SERVER_KEY_CENTER_ONLY", 403);
2124
2243
  });
2125
2244
  }
2126
- runtime2.action(serverKeys.command("revoke <id> <key-id>"), async (ctx, [id2, keyIdValue]) => mutate(ctx, "DELETE", `/v4/creator/contents/${uuid(id2)}/server-keys/${keyId(keyIdValue)}`, void 0, "\uC774 \uC11C\uBC84 \uD0A4\uB97C \uC989\uC2DC \uD3D0\uAE30\uD560\uAE4C\uC694?"));
2245
+ runtime2.action(serverKeys.command("revoke <id> <key-id>").description("\uC11C\uBC84 \uD0A4 \uC989\uC2DC \uD3D0\uAE30 (platform:write)"), async (ctx, [id2, keyIdValue]) => mutate(ctx, "DELETE", `/v4/creator/contents/${uuid(id2)}/server-keys/${keyId(keyIdValue)}`, void 0, "\uC774 \uC11C\uBC84 \uD0A4\uB97C \uC989\uC2DC \uD3D0\uAE30\uD560\uAE4C\uC694?"));
2127
2246
  const shared = content.command("shared").description("\uACF5\uC720 \uBB38\uC11C \uC870\uD68C\xB7\uC228\uAE40\xB7\uC0AD\uC81C (platform:read\xB7platform:write)");
2128
- runtime2.action(shared.command("collections <id>"), async (ctx, [id2]) => ctx.output(await ctx.api.request("GET", `/v4/creator/contents/${uuid(id2)}/shared-document-collections`)));
2129
- runtime2.action(shared.command("list <id> <collection>").option("--limit <count>", "1~100\uAC1C, \uAE30\uBCF8 50"), async (ctx, [id2, collectionValue], opts) => ctx.output(await ctx.api.request(
2247
+ runtime2.action(shared.command("collections <id>").description("\uACF5\uC720 \uBB38\uC11C \uCEEC\uB809\uC158 \uC774\uB984 (platform:read)"), async (ctx, [id2]) => ctx.output(await ctx.api.request("GET", `/v4/creator/contents/${uuid(id2)}/shared-document-collections`)));
2248
+ runtime2.action(shared.command("list <id> <collection>").description("\uACF5\uC720 \uBB38\uC11C \uBAA9\uB85D (platform:read)").option("--limit <count>", "1~100\uAC1C, \uAE30\uBCF8 50"), async (ctx, [id2, collectionValue], opts) => ctx.output(await ctx.api.request(
2130
2249
  "GET",
2131
2250
  `/v4/creator/contents/${uuid(id2)}/shared-documents/${parse(collection, collectionValue)}`,
2132
2251
  { query: { limit: opts.limit } }
2133
2252
  )));
2134
- runtime2.action(shared.command("get <id> <collection> <key>"), async (ctx, [id2, collectionValue, keyValue]) => ctx.output(await ctx.api.request(
2253
+ runtime2.action(shared.command("get <id> <collection> <key>").description("\uACF5\uC720 \uBB38\uC11C \uD558\uB098 \uC870\uD68C (platform:read)"), async (ctx, [id2, collectionValue, keyValue]) => ctx.output(await ctx.api.request(
2135
2254
  "GET",
2136
2255
  `/v4/creator/contents/${uuid(id2)}/shared-documents/${parse(collection, collectionValue)}/${parse(documentKey, keyValue)}`
2137
2256
  )));
2138
- runtime2.action(shared.command("hide <id> <collection> <key>"), async (ctx, [id2, collectionValue, keyValue]) => mutate(
2257
+ runtime2.action(shared.command("hide <id> <collection> <key>").description("\uACF5\uC720 \uBB38\uC11C \uC228\uAE40 (platform:write)"), async (ctx, [id2, collectionValue, keyValue]) => mutate(
2139
2258
  ctx,
2140
2259
  "POST",
2141
2260
  `/v4/creator/contents/${uuid(id2)}/shared-documents/${parse(collection, collectionValue)}/${parse(documentKey, keyValue)}/hide`,
2142
2261
  void 0,
2143
2262
  "\uC774 \uACF5\uC720 \uBB38\uC11C\uB97C \uC228\uAE38\uAE4C\uC694? \uBAA9\uB85D\xB7\uC21C\uC704\uC5D0\uC11C \uC81C\uC678\uB418\uBA70 \uBB38\uC11C \uC790\uCCB4\uB294 \uB0A8\uC2B5\uB2C8\uB2E4."
2144
2263
  ));
2145
- runtime2.action(shared.command("delete <id> <collection> <key>"), async (ctx, [id2, collectionValue, keyValue]) => mutate(
2264
+ runtime2.action(shared.command("delete <id> <collection> <key>").description("\uACF5\uC720 \uBB38\uC11C \uC601\uAD6C \uC0AD\uC81C (platform:write)"), async (ctx, [id2, collectionValue, keyValue]) => mutate(
2146
2265
  ctx,
2147
2266
  "DELETE",
2148
2267
  `/v4/creator/contents/${uuid(id2)}/shared-documents/${parse(collection, collectionValue)}/${parse(documentKey, keyValue)}`,
@@ -4061,7 +4180,12 @@ function serverData(schema, value2) {
4061
4180
  if (!parsed.success) throw new CliError("\uC2A4\uD0AC API \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.", "INVALID_RESPONSE", 502);
4062
4181
  return parsed.data;
4063
4182
  }
4064
- function ensurePat(client, message = "\uC2A4\uD0AC \uC5C5\uB85C\uB4DC\uC5D0\uB294 skill:write \uAD8C\uD55C\uC758 \uAC1C\uC778 \uC561\uC138\uC2A4 \uD1A0\uD070(pat_)\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.") {
4183
+ var PAT_FOR_WRITE = "\uC2A4\uD0AC \uC11C\uBC84 \uAC80\uC99D\xB7\uC5C5\uB85C\uB4DC\xB7\uC644\uB8CC \uCC98\uB9AC\uC5D0\uB294 skill:write \uAD8C\uD55C\uC758 \uAC1C\uC778 \uC561\uC138\uC2A4 \uD1A0\uD070(pat_)\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.";
4184
+ var PAT_FOR_SUBMIT = "\uC2A4\uD0AC \uC2EC\uC0AC \uC81C\uCD9C\uC5D0\uB294 skill:write\uC640 skill:publish \uAD8C\uD55C(\uB458 \uB2E4)\uC758 \uAC1C\uC778 \uC561\uC138\uC2A4 \uD1A0\uD070(pat_)\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.";
4185
+ var PAT_FOR_CANCEL = "\uC2A4\uD0AC \uC5C5\uB85C\uB4DC\xB7\uC2EC\uC0AC \uCDE8\uC18C\uC5D0\uB294 skill:write \uAD8C\uD55C\uC758 \uAC1C\uC778 \uC561\uC138\uC2A4 \uD1A0\uD070(pat_)\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.";
4186
+ var PAT_FOR_RELEASE = "\uC2A4\uD0AC \uAC8C\uC2DC\uC5D0\uB294 skill:publish \uAD8C\uD55C\uC758 \uAC1C\uC778 \uC561\uC138\uC2A4 \uD1A0\uD070(pat_)\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.";
4187
+ var PAT_FOR_DEPRECATE = "\uC2A4\uD0AC \uC9C0\uC6D0 \uC885\uB8CC\uC5D0\uB294 skill:publish \uAD8C\uD55C\uC758 \uAC1C\uC778 \uC561\uC138\uC2A4 \uD1A0\uD070(pat_)\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.";
4188
+ function ensurePat(client, message = PAT_FOR_WRITE) {
4065
4189
  const token = client.settings.token;
4066
4190
  if (!token || !/^pat_[a-f0-9]{64}$/.test(token)) {
4067
4191
  throw new CliError(message, "SKILL_PAT_REQUIRED", 401);
@@ -4146,8 +4270,8 @@ async function pushSkillVersion(client, prepared, skillId) {
4146
4270
  }
4147
4271
  }
4148
4272
  function registerSkillCommands(program2, runtime2) {
4149
- const skill = program2.command("skill").description("\uC2A4\uD0AC \uD328\uD0A4\uC9C0 \uAC80\uC0AC\xB7\uC5C5\uB85C\uB4DC");
4150
- runtime2.action(skill.command("list").option("-q, --query <text>", "\uC2A4\uD0AC \uC774\uB984 \uAC80\uC0C9").option("--category <category>", "\uBD84\uB958").option("--entity <entity>", "\uC0B0\uCD9C\uBB3C \uC5D4\uD2F0\uD2F0(character)").option("--type <type>", "\uC2A4\uD0AC \uC720\uD615(template|instruction|tool)").option("--official <value>", "\uACF5\uC2DD \uC2A4\uD0AC\uB9CC true \uB610\uB294 false").option("--cursor <id>", "\uB2E4\uC74C \uD398\uC774\uC9C0 UUID").option("--all", "\uBAA8\uB4E0 \uD398\uC774\uC9C0 \uC870\uD68C"), async (ctx, _args, opts) => {
4273
+ const skill = program2.command("skill").description("\uC2A4\uD0AC \uD328\uD0A4\uC9C0 \uAC80\uC0AC\xB7\uC5C5\uB85C\uB4DC\xB7\uC2EC\uC0AC\xB7\uAC8C\uC2DC (skill:read\xB7write\xB7publish)");
4274
+ runtime2.action(skill.command("list").description("\uC2A4\uD0AC \uBAA9\uB85D \uAC80\uC0C9 (skill:read)").option("-q, --query <text>", "\uC2A4\uD0AC \uC774\uB984 \uAC80\uC0C9").option("--category <category>", "\uBD84\uB958").option("--entity <entity>", "\uC0B0\uCD9C\uBB3C \uC5D4\uD2F0\uD2F0(character)").option("--type <type>", "\uC2A4\uD0AC \uC720\uD615(template|instruction|tool)").option("--official <value>", "\uACF5\uC2DD \uC2A4\uD0AC\uB9CC true \uB610\uB294 false").option("--cursor <id>", "\uB2E4\uC74C \uD398\uC774\uC9C0 UUID").option("--all", "\uBAA8\uB4E0 \uD398\uC774\uC9C0 \uC870\uD68C"), async (ctx, _args, opts) => {
4151
4275
  const query = opts.query === void 0 ? void 0 : z9.string().trim().min(1).max(100).safeParse(opts.query);
4152
4276
  if (query && !query.success) throw new CliError("\uAC80\uC0C9\uC5B4\uB294 1~100\uC790\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
4153
4277
  const selectedCategory = opts.category === void 0 ? void 0 : category.safeParse(opts.category);
@@ -4189,22 +4313,22 @@ function registerSkillCommands(program2, runtime2) {
4189
4313
  cursor = next;
4190
4314
  }
4191
4315
  });
4192
- runtime2.action(skill.command("get <slug>"), async (ctx, [value2]) => {
4316
+ runtime2.action(skill.command("get <slug>").description("\uC2A4\uD0AC \uC0C1\uC138 \uC870\uD68C (skill:read)"), async (ctx, [value2]) => {
4193
4317
  const parsed = skillSlug.safeParse(value2);
4194
4318
  if (!parsed.success) throw new CliError("\uC2A4\uD0AC \uC774\uB984\uC744 \uD655\uC778\uD558\uC138\uC694.");
4195
4319
  ctx.output(await ctx.api.request("GET", `/v4/skills/${parsed.data}`));
4196
4320
  });
4197
- runtime2.action(skill.command("form <slug> <version>"), async (ctx, [value2, version]) => {
4321
+ runtime2.action(skill.command("form <slug> <version>").description("\uC2A4\uD0AC \uBC84\uC804 \uC785\uB825 \uC591\uC2DD \uC870\uD68C (skill:read)"), async (ctx, [value2, version]) => {
4198
4322
  const parsed = skillSlug.safeParse(value2);
4199
4323
  const parsedVersion = semver.safeParse(version);
4200
4324
  if (!parsed.success || !parsedVersion.success) throw new CliError("\uC2A4\uD0AC \uC774\uB984\uACFC \uBC84\uC804\uC744 \uD655\uC778\uD558\uC138\uC694.");
4201
4325
  ctx.output(await ctx.api.request("GET", `/v4/skills/${parsed.data}/versions/${parsedVersion.data}/form`));
4202
4326
  });
4203
- runtime2.action(skill.command("status <skill-id> <version-id>"), async (ctx, [skillId, versionId]) => {
4327
+ runtime2.action(skill.command("status <skill-id> <version-id>").description("\uB0B4 \uC2A4\uD0AC \uBC84\uC804\uC758 \uC2EC\uC0AC\xB7\uAC8C\uC2DC \uC0C1\uD0DC \uC870\uD68C (skill:read)"), async (ctx, [skillId, versionId]) => {
4204
4328
  const ids = skillVersionIds(skillId, versionId);
4205
4329
  ctx.output(await ctx.api.request("GET", `/v4/creator/skills/${ids.skillId}/versions/${ids.versionId}`));
4206
4330
  });
4207
- runtime2.action(skill.command("validate [dir]").option("--remote", "\uC11C\uBC84\uC5D0\uC11C\uB3C4 \uD328\uD0A4\uC9C0\uB97C \uB4DC\uB77C\uC774\uB7F0 \uAC80\uC99D"), async (ctx, [dir], opts) => {
4331
+ 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) => {
4208
4332
  const prepared = await prepareSkillPackage(dir ?? ".");
4209
4333
  if (opts.remote && !ctx.options.dryRun) {
4210
4334
  const remote = await validateSkillRemotely(ctx.api, prepared.bytes);
@@ -4225,7 +4349,7 @@ function registerSkillCommands(program2, runtime2) {
4225
4349
  ...opts.remote ? { remote_skipped: true } : {}
4226
4350
  });
4227
4351
  });
4228
- runtime2.action(skill.command("push [dir]").option("--skill-id <id>", "\uAE30\uC874 \uC2A4\uD0AC ID\uC5D0 \uC0C8 \uBC84\uC804 \uCD94\uAC00"), async (ctx, [dir], opts) => {
4352
+ 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) => {
4229
4353
  const prepared = await prepareSkillPackage(dir ?? ".");
4230
4354
  if (prepared.manifest.type !== "instruction" && prepared.manifest.type !== "template") {
4231
4355
  throw new CliError("\uD604\uC7AC \uC2A4\uD0AC \uC5C5\uB85C\uB4DC\uB294 instruction\xB7template \uC720\uD615\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4.", "SKILL_TYPE_UNSUPPORTED");
@@ -4247,7 +4371,7 @@ function registerSkillCommands(program2, runtime2) {
4247
4371
  }
4248
4372
  ctx.output(await pushSkillVersion(ctx.api, prepared, skillIdOption?.data));
4249
4373
  });
4250
- runtime2.action(skill.command("complete <skill-id> <version-id>"), async (ctx, [skillId, versionId]) => {
4374
+ runtime2.action(skill.command("complete <skill-id> <version-id>").description("\uC5C5\uB85C\uB4DC\uD55C \uC2A4\uD0AC \uBC84\uC804 \uC644\uB8CC \uCC98\uB9AC \uC7AC\uC2DC\uB3C4 (skill:write)"), async (ctx, [skillId, versionId]) => {
4251
4375
  ensurePat(ctx.api);
4252
4376
  const parsedSkillId = uuid2.safeParse(skillId);
4253
4377
  const parsedVersionId = uuid2.safeParse(versionId);
@@ -4258,8 +4382,8 @@ function registerSkillCommands(program2, runtime2) {
4258
4382
  }
4259
4383
  ctx.output(await ctx.api.request("POST", `/v4/creator/skills/${parsedSkillId.data}/versions/${parsedVersionId.data}/complete`));
4260
4384
  });
4261
- runtime2.action(skill.command("submit <skill-id> <version-id>"), async (ctx, [skillId, versionId]) => {
4262
- ensurePat(ctx.api, "\uC2A4\uD0AC \uC2EC\uC0AC \uC81C\uCD9C\uC5D0\uB294 skill:write\uC640 skill:publish \uAD8C\uD55C\uC758 \uAC1C\uC778 \uC561\uC138\uC2A4 \uD1A0\uD070(pat_)\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
4385
+ runtime2.action(skill.command("submit <skill-id> <version-id>").description("\uAC80\uC99D \uC644\uB8CC\uD55C \uC2A4\uD0AC \uBC84\uC804 \uC2EC\uC0AC \uC81C\uCD9C (skill:write\uC640 skill:publish \uB458 \uB2E4 \uD544\uC694)"), async (ctx, [skillId, versionId]) => {
4386
+ ensurePat(ctx.api, PAT_FOR_SUBMIT);
4263
4387
  const ids = skillVersionIds(skillId, versionId);
4264
4388
  const path = `/v4/creator/skills/${ids.skillId}/versions/${ids.versionId}`;
4265
4389
  const current = serverData(managedVersionSchema, (await ctx.api.request("GET", path)).data);
@@ -4279,8 +4403,8 @@ function registerSkillCommands(program2, runtime2) {
4279
4403
  await ctx.confirm(`${current.version} \uBC84\uC804(\uD328\uD0A4\uC9C0 ${current.package_hash})\uC744 \uC2EC\uC0AC\uC5D0 \uC81C\uCD9C\uD560\uAE4C\uC694?`);
4280
4404
  ctx.output(await ctx.api.request("POST", `${path}/submit`));
4281
4405
  });
4282
- runtime2.action(skill.command("deprecate <skill-id>").description("\uC2A4\uD0AC\uC744 \uC9C0\uC6D0 \uC885\uB8CC\uD55C\uB2E4 (\uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC74C, \uC0C8 \uC124\uCE58\xB7\uC0C8 \uC81C\uC791 \uBD88\uAC00)"), async (ctx, [skillId]) => {
4283
- ensurePat(ctx.api);
4406
+ runtime2.action(skill.command("deprecate <skill-id>").description("\uC2A4\uD0AC\uC744 \uC9C0\uC6D0 \uC885\uB8CC\uD55C\uB2E4 (\uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC74C, \uC0C8 \uC124\uCE58\xB7\uC0C8 \uC81C\uC791 \uBD88\uAC00, skill:publish)"), async (ctx, [skillId]) => {
4407
+ ensurePat(ctx.api, PAT_FOR_DEPRECATE);
4284
4408
  const parsedSkillId = uuid2.safeParse(skillId);
4285
4409
  if (!parsedSkillId.success) throw new CliError("\uC2A4\uD0AC ID\uB294 UUID\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
4286
4410
  const path = `/v4/creator/skills/${parsedSkillId.data}/deprecate`;
@@ -4291,8 +4415,8 @@ function registerSkillCommands(program2, runtime2) {
4291
4415
  await ctx.confirm("\uC774 \uC2A4\uD0AC\uC744 \uC9C0\uC6D0 \uC885\uB8CC\uD560\uAE4C\uC694? \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC73C\uBA70 \uC0C8 \uC124\uCE58\xB7\uC0C8 \uC81C\uC791\uC774 \uBD88\uAC00\uB2A5\uD569\uB2C8\uB2E4(\uAE30\uC874 \uCF58\uD150\uCE20\uC758 \uACE0\uC815 \uBC84\uC804\uC740 \uC720\uC9C0\uB429\uB2C8\uB2E4).");
4292
4416
  ctx.output(await ctx.api.request("POST", path));
4293
4417
  });
4294
- runtime2.action(skill.command("cancel <skill-id> <version-id>"), async (ctx, [skillId, versionId]) => {
4295
- ensurePat(ctx.api);
4418
+ runtime2.action(skill.command("cancel <skill-id> <version-id>").description("\uC2A4\uD0AC \uBC84\uC804\uC758 \uC5C5\uB85C\uB4DC \uB610\uB294 \uC2EC\uC0AC \uCDE8\uC18C (skill:write)"), async (ctx, [skillId, versionId]) => {
4419
+ ensurePat(ctx.api, PAT_FOR_CANCEL);
4296
4420
  const ids = skillVersionIds(skillId, versionId);
4297
4421
  const path = `/v4/creator/skills/${ids.skillId}/versions/${ids.versionId}`;
4298
4422
  if (ctx.options.dryRun) {
@@ -4303,9 +4427,9 @@ function registerSkillCommands(program2, runtime2) {
4303
4427
  ctx.output(await ctx.api.request("POST", `${path}/cancel`));
4304
4428
  });
4305
4429
  runtime2.action(
4306
- skill.command("release <skill-id> <version-id>").requiredOption("--visibility <value>", "private|unlisted|public"),
4430
+ skill.command("release <skill-id> <version-id>").description("\uC2B9\uC778\uB41C \uC2A4\uD0AC \uBC84\uC804\uC744 \uACF5\uAC1C \uBC94\uC704\uB85C \uAC8C\uC2DC (skill:publish)").requiredOption("--visibility <value>", "private|unlisted|public"),
4307
4431
  async (ctx, [skillId, versionId], opts) => {
4308
- ensurePat(ctx.api);
4432
+ ensurePat(ctx.api, PAT_FOR_RELEASE);
4309
4433
  const ids = skillVersionIds(skillId, versionId);
4310
4434
  const selected = visibility.safeParse(opts.visibility);
4311
4435
  if (!selected.success) throw new CliError("\uACF5\uAC1C \uBC94\uC704\uB294 private, unlisted, public \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
@@ -4387,7 +4511,7 @@ function parsePat(schema, input, label) {
4387
4511
  return parsed.data;
4388
4512
  }
4389
4513
  var toolDefinitions = [
4390
- { 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.", inputSchema: { type: "object", ...z10.toJSONSchema(authoringInput) }, annotations: { readOnlyHint: false, destructiveHint: false } },
4514
+ { 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 } },
4391
4515
  {
4392
4516
  name: "platform_usage_get",
4393
4517
  description: "\uC11C\uBC84 \uD0A4\uAC00 \uC18D\uD55C \uCF58\uD150\uCE20\uC758 \uD50C\uB7AB\uD3FC \uC0AC\uC6A9\uB7C9\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4.",
@@ -4433,13 +4557,13 @@ var toolDefinitions = [
4433
4557
  // 아래는 개인 액세스 토큰(CLACK_TOKEN, pat_)으로 동작하는 개인 도구다. 서버 키 도구와 인증 평면이 다르다.
4434
4558
  {
4435
4559
  name: "platform_skill_list",
4436
- description: "\uACF5\uAC1C \uC2A4\uD0AC \uBAA9\uB85D\uC744 \uC774\uB984\uC73C\uB85C \uAC80\uC0C9\uD569\uB2C8\uB2E4.",
4560
+ description: "\uACF5\uAC1C \uC2A4\uD0AC \uBAA9\uB85D\uC744 \uC774\uB984\uC73C\uB85C \uAC80\uC0C9\uD569\uB2C8\uB2E4. skill:read \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.",
4437
4561
  inputSchema: z10.toJSONSchema(patSchemas.skillList),
4438
4562
  annotations: { readOnlyHint: true, destructiveHint: false }
4439
4563
  },
4440
4564
  {
4441
4565
  name: "platform_skill_get",
4442
- description: "\uC2A4\uD0AC \uC0C1\uC138\uB97C \uC774\uB984(slug)\uC73C\uB85C \uC870\uD68C\uD569\uB2C8\uB2E4.",
4566
+ description: "\uC2A4\uD0AC \uC0C1\uC138\uB97C \uC774\uB984(slug)\uC73C\uB85C \uC870\uD68C\uD569\uB2C8\uB2E4. skill:read \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.",
4443
4567
  inputSchema: z10.toJSONSchema(patSchemas.skillGet),
4444
4568
  annotations: { readOnlyHint: true, destructiveHint: false }
4445
4569
  },
@@ -4451,13 +4575,13 @@ var toolDefinitions = [
4451
4575
  },
4452
4576
  {
4453
4577
  name: "platform_skill_status",
4454
- description: "\uB0B4 \uC2A4\uD0AC \uBC84\uC804\uC758 \uC2EC\uC0AC\xB7\uAC8C\uC2DC \uC0C1\uD0DC\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.",
4578
+ description: "\uB0B4 \uC2A4\uD0AC \uBC84\uC804\uC758 \uC2EC\uC0AC\xB7\uAC8C\uC2DC \uC0C1\uD0DC\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. skill:read \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.",
4455
4579
  inputSchema: z10.toJSONSchema(patSchemas.skillVersionRef),
4456
4580
  annotations: { readOnlyHint: true, destructiveHint: false }
4457
4581
  },
4458
4582
  {
4459
4583
  name: "platform_skill_submit",
4460
- description: "\uAC80\uC99D \uC644\uB8CC\uD55C \uC2A4\uD0AC \uBC84\uC804\uC744 \uC2EC\uC0AC\uC5D0 \uC81C\uCD9C\uD569\uB2C8\uB2E4. skill:write\xB7skill:publish \uAD8C\uD55C\uACFC confirm:true\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
4584
+ description: "\uAC80\uC99D \uC644\uB8CC\uD55C \uC2A4\uD0AC \uBC84\uC804\uC744 \uC2EC\uC0AC\uC5D0 \uC81C\uCD9C\uD569\uB2C8\uB2E4. skill:write\uC640 skill:publish \uAD8C\uD55C(\uB458 \uB2E4)\uACFC confirm:true\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
4461
4585
  inputSchema: z10.toJSONSchema(patSchemas.skillSubmit),
4462
4586
  annotations: { readOnlyHint: false, destructiveHint: true }
4463
4587
  },
@@ -4574,7 +4698,7 @@ var PlatformMcpServer = class {
4574
4698
  return { jsonrpc: "2.0", id: request.id, result: toolResult({ error: failure }, true) };
4575
4699
  }
4576
4700
  }
4577
- /** 개인 액세스 토큰(platform:read·platform:write와 skill:*, creator-content:publish 스코프) 기반 도구. */
4701
+ /** 개인 액세스 토큰(platform:read·platform:write, skill:*, creator-content:write·publish 스코프) 기반 도구. */
4578
4702
  async callPatTool(name2, args) {
4579
4703
  if (patDestructiveToolNames.has(name2) && (!isObject2(args) || args.confirm !== true)) {
4580
4704
  return { preview: true, confirmation_required: true, tool: name2, arguments: redact(args) };
@@ -4655,6 +4779,19 @@ async function servePlatformMcp(settings, env = process.env) {
4655
4779
 
4656
4780
  // src/commands/integrations.ts
4657
4781
  var SKILLS_REPOSITORY = "https://github.com/clack-project/skills";
4782
+ var AGENT_SKILLS = [
4783
+ { name: "clack-setup", audience: "general", minimum_cli_version: "0.1.1", cli_required: true },
4784
+ { name: "clack-products", audience: "general", minimum_cli_version: "0.1.1", cli_required: true },
4785
+ { name: "clack-content", audience: "general", minimum_cli_version: "0.1.1", cli_required: true },
4786
+ { name: "clack-channel", audience: "general", minimum_cli_version: "0.1.1", cli_required: true },
4787
+ { name: "clack-profile", audience: "general", minimum_cli_version: "0.1.1", cli_required: true },
4788
+ { name: "clack-mcp", audience: "general", minimum_cli_version: "0.1.0", cli_required: false },
4789
+ { name: "clack-creator-content", audience: "creator", minimum_cli_version: "0.1.1", cli_required: true },
4790
+ { name: "clack-page", audience: "creator", minimum_cli_version: "0.1.1", cli_required: true },
4791
+ { name: "clack-skill-package", audience: "creator", minimum_cli_version: "0.1.1", cli_required: true },
4792
+ { name: "clack-platform-data", audience: "creator", minimum_cli_version: "0.1.1", cli_required: true }
4793
+ ];
4794
+ var SKILLS_MINIMUM_CLI_VERSION = "0.1.1";
4658
4795
  function quoteArgument(value2) {
4659
4796
  return `'${value2.replaceAll("'", "'\\''")}'`;
4660
4797
  }
@@ -4687,7 +4824,7 @@ function registerIntegrationCommands(program2, runtime2) {
4687
4824
  const mcp = program2.command("mcp").description("MCP \uD074\uB77C\uC774\uC5B8\uD2B8 \uC5F0\uACB0\xB7\uD50C\uB7AB\uD3FC \uC870\uD68C \uB3C4\uAD6C");
4688
4825
  runtime2.action(mcp.command("config").description("\uD1A0\uD070 \uC5C6\uB294 MCP \uC124\uC815 \uCD9C\uB825 (\uD30C\uC77C \uBCC0\uACBD\xB7\uC11C\uBC84 \uC694\uCCAD \uC5C6\uC74C)").option("--platform", "CLACK_SERVER_KEY\uB97C \uC0AC\uC6A9\uD558\uB294 \uB85C\uCEEC \uD50C\uB7AB\uD3FC \uC870\uD68C \uC11C\uBC84 \uC124\uC815").addOption(new Option2("--print", "Claude Code .mcp.json \uC870\uAC01 \uCD9C\uB825 (\uAE30\uBCF8)").conflicts(["claude", "codex"])).addOption(new Option2("--claude", "Claude Code \uB4F1\uB85D \uBA85\uB839 \uCD9C\uB825").conflicts(["print", "codex"])).addOption(new Option2("--codex", "Codex config.toml \uC870\uAC01 \uCD9C\uB825").conflicts(["print", "claude"])), async (ctx, _args, opts) => {
4689
4826
  const config = opts.platform ? platformMcpConfiguration(ctx.api.settings.baseUrl) : mcpConfiguration(ctx.api.settings.baseUrl);
4690
- const instructions = opts.platform ? "\uC5D0\uC774\uC804\uD2B8 \uC2E4\uD589 \uD658\uACBD\uC5D0 \uCF58\uD150\uCE20 \uC11C\uBC84 \uD0A4 \uB3C4\uAD6C\uB294 \uBC1C\uAE09\uBC1B\uC740 \uC11C\uBC84 \uD0A4\uB97C CLACK_SERVER_KEY\uB85C, \uC2A4\uD0AC\xB7\uCF58\uD150\uCE20 \uACF5\uAC1C\xB7\uACF5\uC720 \uBB38\uC11C \uB3C4\uAD6C\uB294 platform:read\xB7platform:write(\uD544\uC694 \uC2DC skill:*\xB7creator-content:publish) \uAD8C\uD55C\uC758 \uAC1C\uC778 \uC561\uC138\uC2A4 \uD1A0\uD070\uC744 CLACK_TOKEN\uC73C\uB85C \uC124\uC815\uD558\uC138\uC694. \uD544\uC694\uD55C \uB3C4\uAD6C\uB9CC\uD07C\uB9CC \uC124\uC815\uD558\uBA74 \uB429\uB2C8\uB2E4. \uC124\uC815 \uCD9C\uB825\uC5D0\uB294 \uD0A4\xB7\uD1A0\uD070 \uC6D0\uBB38\uC744 \uB123\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uD658\uACBD\uACFC \uC124\uC815 \uC6D0\uC810\uC774 \uC77C\uCE58\uD558\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694." : "\uC571\uC5D0\uC11C \uBC1C\uAE09\uD55C \uAC1C\uC778 \uC561\uC138\uC2A4 \uD1A0\uD070\uC744 \uC5D0\uC774\uC804\uD2B8 \uC2E4\uD589 \uD658\uACBD\uC758 CLACK_TOKEN\uC5D0 \uC548\uC804\uD558\uAC8C \uC124\uC815\uD558\uC138\uC694. CLI\uC5D0 \uC800\uC7A5\uB41C \uD1A0\uD070\uC740 \uC790\uB3D9 \uC804\uB2EC\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uD1A0\uD070\uC744 \uB300\uD654\xB7\uBA85\uB839 \uAE30\uB85D\xB7\uC124\uC815 \uD30C\uC77C\uC5D0 \uBD99\uC5EC \uB123\uC9C0 \uB9C8\uC138\uC694. CLI \uD504\uB85C\uD544\uC744 \uBC14\uAFB8\uC5B4\uB3C4 \uAE30\uC874 MCP \uC124\uC815\uC740 \uAC31\uC2E0\uB418\uC9C0 \uC54A\uC73C\uBBC0\uB85C URL\uACFC \uD1A0\uD070\uC758 \uACC4\uC815\uC744 \uD655\uC778\uD558\uC138\uC694.";
4827
+ const instructions = opts.platform ? "\uC5D0\uC774\uC804\uD2B8 \uC2E4\uD589 \uD658\uACBD\uC5D0 \uCF58\uD150\uCE20 \uC11C\uBC84 \uD0A4 \uB3C4\uAD6C\uB294 \uBC1C\uAE09\uBC1B\uC740 \uC11C\uBC84 \uD0A4\uB97C CLACK_SERVER_KEY\uB85C, \uC2A4\uD0AC\xB7\uC81C\uC791\xB7\uCF58\uD150\uCE20 \uACF5\uAC1C\xB7\uACF5\uC720 \uBB38\uC11C \uB3C4\uAD6C\uB294 platform:read\xB7platform:write(\uD544\uC694 \uC2DC skill:*, \uC81C\uC791\uC740 creator-content:write, \uCF58\uD150\uCE20 \uACF5\uAC1C\uB294 creator-content:publish) \uAD8C\uD55C\uC758 \uAC1C\uC778 \uC561\uC138\uC2A4 \uD1A0\uD070\uC744 CLACK_TOKEN\uC73C\uB85C \uC124\uC815\uD558\uC138\uC694. \uD544\uC694\uD55C \uB3C4\uAD6C\uB9CC\uD07C\uB9CC \uC124\uC815\uD558\uBA74 \uB429\uB2C8\uB2E4. \uC124\uC815 \uCD9C\uB825\uC5D0\uB294 \uD0A4\xB7\uD1A0\uD070 \uC6D0\uBB38\uC744 \uB123\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uD658\uACBD\uACFC \uC124\uC815 \uC6D0\uC810\uC774 \uC77C\uCE58\uD558\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694." : "\uC571\uC5D0\uC11C \uBC1C\uAE09\uD55C \uAC1C\uC778 \uC561\uC138\uC2A4 \uD1A0\uD070\uC744 \uC5D0\uC774\uC804\uD2B8 \uC2E4\uD589 \uD658\uACBD\uC758 CLACK_TOKEN\uC5D0 \uC548\uC804\uD558\uAC8C \uC124\uC815\uD558\uC138\uC694. CLI\uC5D0 \uC800\uC7A5\uB41C \uD1A0\uD070\uC740 \uC790\uB3D9 \uC804\uB2EC\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uD1A0\uD070\uC744 \uB300\uD654\xB7\uBA85\uB839 \uAE30\uB85D\xB7\uC124\uC815 \uD30C\uC77C\uC5D0 \uBD99\uC5EC \uB123\uC9C0 \uB9C8\uC138\uC694. CLI \uD504\uB85C\uD544\uC744 \uBC14\uAFB8\uC5B4\uB3C4 \uAE30\uC874 MCP \uC124\uC815\uC740 \uAC31\uC2E0\uB418\uC9C0 \uC54A\uC73C\uBBC0\uB85C URL\uACFC \uD1A0\uD070\uC758 \uACC4\uC815\uC744 \uD655\uC778\uD558\uC138\uC694.";
4691
4828
  if (ctx.options.json) process.stdout.write(JSON.stringify({ ok: true, data: {
4692
4829
  ...config,
4693
4830
  token_env: opts.platform ? "CLACK_SERVER_KEY" : "CLACK_TOKEN",
@@ -4705,11 +4842,14 @@ function registerIntegrationCommands(program2, runtime2) {
4705
4842
  runtime2.action(program2.command("skills").description("\uC5D0\uC774\uC804\uD2B8 \uC2A4\uD0AC \uC124\uCE58 \uBC29\uBC95 \uC548\uB0B4"), async (ctx) => {
4706
4843
  ctx.output({
4707
4844
  repository: SKILLS_REPOSITORY,
4708
- minimum_cli_version: "0.1.0",
4845
+ minimum_cli_version: SKILLS_MINIMUM_CLI_VERSION,
4709
4846
  cli_version: VERSION,
4847
+ skill_count: AGENT_SKILLS.length,
4848
+ skills: AGENT_SKILLS,
4710
4849
  install: "npx skills add clack-project/skills",
4711
4850
  install_global: "npx skills add clack-project/skills -g",
4712
4851
  install_selected: "npx skills add clack-project/skills -s clack-products",
4852
+ install_selected_multiple: "npx skills add clack-project/skills --skill clack-setup clack-creator-content clack-page",
4713
4853
  update: "npx skills update",
4714
4854
  remove: "npx skills remove",
4715
4855
  message: `\uACF5\uAC1C \uC800\uC7A5\uC18C ${SKILLS_REPOSITORY}\uC5D0\uC11C npx skills\uB85C \uC124\uCE58\uD569\uB2C8\uB2E4. \uC774 \uBA85\uB839\uC740 \uC548\uB0B4\uB9CC \uCD9C\uB825\uD558\uBA70 \uC124\uCE58\uB098 \uB85C\uADF8\uC778\uC744 \uC2E4\uD589\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`
@@ -4730,14 +4870,14 @@ var presentationSchema = z11.object({
4730
4870
  }).strict();
4731
4871
  function registerPageCommands(program2, runtime2) {
4732
4872
  const page = program2.command("page").description("\uC138\uACC4\uAD00\xB7\uB0B4 \uACF5\uAC1C \uD648 HTML \uAFB8\uBBF8\uAE30 (custom-page:read\xB7write\xB7publish)");
4733
- runtime2.action(page.command("list"), async (ctx) => ctx.output(await ctx.api.request("GET", base)));
4734
- runtime2.action(page.command("status <id>"), async (ctx, [id2]) => ctx.output(await ctx.api.request("GET", `${base}/${uuid3(id2)}`)));
4735
- runtime2.action(page.command("create").addOption(new Option3("--target <type>", "\uAFB8\uBC00 \uB300\uC0C1").choices(["profile", "space"]).makeOptionMandatory()).option("--space-id <uuid>", "\uC138\uACC4\uAD00 ID").requiredOption("--policy-version <version>", "\uB3D9\uC758\uD55C \uCF58\uD150\uCE20 \uC81C\uACF5 \uC815\uCC45 \uBC84\uC804"), async (ctx, _args, opts) => {
4873
+ runtime2.action(page.command("list").description("\uB0B4 \uD398\uC774\uC9C0 \uBAA9\uB85D (custom-page:read)"), async (ctx) => ctx.output(await ctx.api.request("GET", base)));
4874
+ runtime2.action(page.command("status <id>").description("\uD398\uC774\uC9C0\uC640 \uBC84\uC804\xB7\uC218\uC815 \uBC88\uD638 \uC0C1\uD0DC (custom-page:read)"), async (ctx, [id2]) => ctx.output(await ctx.api.request("GET", `${base}/${uuid3(id2)}`)));
4875
+ runtime2.action(page.command("create").description("\uAFB8\uBC00 \uD398\uC774\uC9C0 \uB4F1\uB85D (custom-page:write)").addOption(new Option3("--target <type>", "\uAFB8\uBC00 \uB300\uC0C1").choices(["profile", "space"]).makeOptionMandatory()).option("--space-id <uuid>", "\uC138\uACC4\uAD00 ID").requiredOption("--policy-version <version>", "\uB3D9\uC758\uD55C \uCF58\uD150\uCE20 \uC81C\uACF5 \uC815\uCC45 \uBC84\uC804"), async (ctx, _args, opts) => {
4736
4876
  if (opts.target === "profile" && opts.spaceId) throw new CliError("\uB0B4 \uACF5\uAC1C \uD648\uC5D0\uB294 \uC138\uACC4\uAD00 ID\uB97C \uC9C0\uC815\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
4737
4877
  const policy = parse(z11.literal("2026-09-22"), opts.policyVersion);
4738
4878
  await mutate(ctx, "POST", base, { target_type: opts.target, ...opts.target === "space" ? { space_id: uuid3(opts.spaceId) } : {}, policy_version: policy });
4739
4879
  });
4740
- runtime2.action(page.command("upload <id> <file>").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) => {
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) => {
4741
4881
  const path = `${base}/${uuid3(id2)}`;
4742
4882
  const prepared = await prepareContentFile(file);
4743
4883
  const presentation = parse(presentationSchema, { schema_version: 1, header_mode: opts.header, color_scheme: opts.color });
@@ -4754,16 +4894,16 @@ function registerPageCommands(program2, runtime2) {
4754
4894
  throw failure;
4755
4895
  }
4756
4896
  });
4757
- runtime2.action(page.command("complete <id> <upload-id>"), async (ctx, [id2, uploadId]) => mutate(ctx, "POST", `${base}/${uuid3(id2)}/uploads/${uuid3(uploadId)}/complete`, {}));
4758
- runtime2.action(page.command("presentation <id> <version-id>").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`, {
4897
+ 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`, {
4759
4899
  presentation: parse(presentationSchema, { schema_version: 1, header_mode: opts.header, color_scheme: opts.color })
4760
4900
  }));
4761
- runtime2.action(page.command("preview <id> <version-id>"), async (ctx, [id2, versionId]) => ctx.output(await ctx.api.request("GET", `${base}/${uuid3(id2)}/versions/${uuid3(versionId)}/preview`)));
4762
- runtime2.action(page.command("submit <id> <version-id>"), async (ctx, [id2, versionId]) => mutate(ctx, "POST", `${base}/${uuid3(id2)}/versions/${uuid3(versionId)}/submit`, {}, "\uC571\uC5D0\uC11C \uD655\uC778\uD55C \uC774 \uBC84\uC804\uC758 \uC2EC\uC0AC\uB97C \uC694\uCCAD\uD560\uAE4C\uC694? \uC2B9\uC778 \uB4A4 \uC9C1\uC811 \uC801\uC6A9\uD574\uC57C \uD569\uB2C8\uB2E4."));
4901
+ 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`)));
4902
+ runtime2.action(page.command("submit <id> <version-id>").description("\uC571\uC5D0\uC11C \uD655\uC778 \uC644\uB8CC\uD55C \uBC84\uC804\uC758 \uC2EC\uC0AC \uC694\uCCAD (custom-page:write\uC640 custom-page:publish \uB458 \uB2E4 \uD544\uC694)"), async (ctx, [id2, versionId]) => mutate(ctx, "POST", `${base}/${uuid3(id2)}/versions/${uuid3(versionId)}/submit`, {}, "\uC571\uC5D0\uC11C \uD655\uC778\uD55C \uC774 \uBC84\uC804\uC758 \uC2EC\uC0AC\uB97C \uC694\uCCAD\uD560\uAE4C\uC694? \uC2B9\uC778 \uB4A4 \uC9C1\uC811 \uC801\uC6A9\uD574\uC57C \uD569\uB2C8\uB2E4."));
4763
4903
  for (const name2 of ["apply", "restore"]) {
4764
- runtime2.action(page.command(`${name2} <id> <version-id>`).requiredOption("--revision <number>", "status\uC5D0\uC11C \uD655\uC778\uD55C \uD604\uC7AC \uC218\uC815 \uBC88\uD638"), async (ctx, [id2, versionId], opts) => mutate(ctx, "POST", `${base}/${uuid3(id2)}/apply`, { version_id: uuid3(versionId), revision: revision3(opts.revision) }, "\uC2B9\uC778\uB41C \uC774 \uBC84\uC804\uC758 \uD30C\uC77C\uACFC \uD5E4\uB354 \uC124\uC815\uC744 \uC801\uC6A9\uD560\uAE4C\uC694?"));
4904
+ runtime2.action(page.command(`${name2} <id> <version-id>`).description(`${name2 === "apply" ? "\uC2B9\uC778\uB41C \uBC84\uC804 \uC801\uC6A9" : "\uC774\uC804 \uC2B9\uC778 \uBC84\uC804\uC73C\uB85C \uBCF5\uC6D0"} (custom-page:publish)`).requiredOption("--revision <number>", "status\uC5D0\uC11C \uD655\uC778\uD55C \uD604\uC7AC \uC218\uC815 \uBC88\uD638"), async (ctx, [id2, versionId], opts) => mutate(ctx, "POST", `${base}/${uuid3(id2)}/apply`, { version_id: uuid3(versionId), revision: revision3(opts.revision) }, "\uC2B9\uC778\uB41C \uC774 \uBC84\uC804\uC758 \uD30C\uC77C\uACFC \uD5E4\uB354 \uC124\uC815\uC744 \uC801\uC6A9\uD560\uAE4C\uC694?"));
4765
4905
  }
4766
- runtime2.action(page.command("disable <id>").requiredOption("--revision <number>", "status\uC5D0\uC11C \uD655\uC778\uD55C \uD604\uC7AC \uC218\uC815 \uBC88\uD638"), async (ctx, [id2], opts) => mutate(ctx, "POST", `${base}/${uuid3(id2)}/disable`, { revision: revision3(opts.revision) }, "\uAE30\uBCF8 \uD654\uBA74\uC73C\uB85C \uC804\uD658\uD560\uAE4C\uC694? \uC5C5\uB85C\uB4DC\uD55C \uBC84\uC804\uC740 \uBCF4\uC874\uB429\uB2C8\uB2E4."));
4906
+ runtime2.action(page.command("disable <id>").description("\uAE30\uBCF8 \uD654\uBA74\uC73C\uB85C \uC804\uD658 (custom-page:publish)").requiredOption("--revision <number>", "status\uC5D0\uC11C \uD655\uC778\uD55C \uD604\uC7AC \uC218\uC815 \uBC88\uD638"), async (ctx, [id2], opts) => mutate(ctx, "POST", `${base}/${uuid3(id2)}/disable`, { revision: revision3(opts.revision) }, "\uAE30\uBCF8 \uD654\uBA74\uC73C\uB85C \uC804\uD658\uD560\uAE4C\uC694? \uC5C5\uB85C\uB4DC\uD55C \uBC84\uC804\uC740 \uBCF4\uC874\uB429\uB2C8\uB2E4."));
4767
4907
  }
4768
4908
 
4769
4909
  // src/cli.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clack-platform/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "클랙 계정의 상품·콘텐츠·프로필을 관리하는 CLI",
5
5
  "type": "module",
6
6
  "bin": {