# democut

DemoCut(democut) 에이전트 인증 CLI — OAuth2 **Device Flow**(RFC 8628). Higgsfield 의
`auth login` 과 같은 방식으로, 에이전트가 한 번 로그인하면 워크스페이스 스코프 토큰을 받아
이후 호출에 Bearer 가 자동으로 붙는다.

## 설치

```bash
npm install -g democut               # 전역 설치 (선택)
npx -y democut@latest auth login     # 설치 없이 바로 (@latest 필수 — 아래 MCP 절 참고)
```

## 사용

아래 예시는 **전역 설치한 경우**다. npx 로 쓰고 있다면 `democut` 을
`npx -y democut@latest` 로 바꿔 읽으면 된다.

```bash
democut auth login      # device flow 로 로그인 → 토큰 저장
democut auth status     # 현재 정체/워크스페이스/티어
democut auth logout

# 헤드리스(서버/CI/에이전트): 브라우저 안 열고 코드+URL 만 출력
democut auth login --no-browser
# 기계 판독(래퍼가 코드를 Slack 등에 게시):
democut auth login --json
```

`--base-url <url>` 또는 `DEMOCUT_BASE_URL` 로 서버를 지정한다(기본: prod).

## 동작

1. `auth login` 이 서버에 device authorization 요청 → `user_code` + 승인 URL 수신.
2. 데스크톱이면 브라우저 자동 오픈, 헤드리스면 코드+URL 출력 → **사람이 브라우저에서
   로그인하고 워크스페이스(조직)를 선택해 승인**.
3. CLI 가 토큰 엔드포인트를 폴링 → 승인되면 `access`/`refresh` 토큰을 `~/.democut/credentials.json`
   (권한 600)에 저장.
4. 이후 `access`(1h)는 만료 시 `refresh`(장수명)로 자동 갱신.

토큰은 **승인한 사람의 워크스페이스와 플랜(tier/role)** 으로 스코프된다 — 워크스페이스마다
각자의 에이전트를 붙일 수 있는 멀티테넌트 구조.

## 명령 — MCP 도구와 **같은 이름**

`democut mcp` 가 에이전트에게 주는 도구를 터미널·스크립트에서도 그대로 부른다. 이름은 도구 이름의
밑줄만 하이픈으로 바꾼 것이고, 밑줄·`democut_` 접두사 형태도 그대로 받는다.

```bash
democut_estimate_cuts   (MCP 도구)
democut estimate-cuts   (CLI — 같은 인자, 같은 결과)
```

명령·인자·설명은 **손으로 쓰지 않는다.** 서버의 도구 표(`GET /api/meta/tools`)를 빌드 때 받아
생성하므로 계약과 어긋날 자리가 없다.

```bash
democut --help                  # 묶음별 명령 목록
democut help export             # 한 묶음만 자세히
democut estimate-cuts --help    # 그 명령의 인자
```

### 인자 주는 법

플래그로 주거나, MCP 호출 본문을 통째로 넘긴다.

```bash
# 플래그 — 사람이 치기 좋다
democut estimate-cuts --slug demo --items '[{"cut_id":"c1","mode":"ai_video"}]'

# 인자 객체 그대로 — 에이전트·스크립트가 MCP 호출을 그대로 옮길 때
democut estimate-cuts --args-json '{"slug":"demo","items":[…]}'
echo '{"slug":"demo","items":[…]}' | democut estimate-cuts --args-json @-
democut estimate-cuts --args-json @payload.json
```

둘을 함께 주면 **플래그가 이긴다**(통째로 준 본문 위에 한 값만 덮어쓰는 것이 자연스럽다).
불리언은 값 없이 켜고(`--transcribe`) `--no-` 로 끈다. 배열·객체 인자는 JSON 문자열이다.
모르는 인자는 **조용히 무시하지 않고 거부한다** — 삼키면 넣은 값이 오류 없이 사라진다.

### 출력

기본은 사람이 읽는 문장이고, `--json` 은 성공·실패를 같은 모양으로 stdout 에 낸다.

```bash
democut get-storyboard --slug demo --json
# {"ok":true,"text":"…","data":{…}}       ← data 는 MCP 의 structuredContent 와 같은 값
```

실패도 stdout 에 같은 모양으로 나온다(파이프로 읽는 쪽이 두 경로를 합치지 않아도 되게).
실패라는 사실은 **종료 코드**가 말한다.

### 비용 승인

돈이 나가는 명령은 승인 없이 진행하지 않는다. 대상은 계약이 정한다(도움말의 `[과금]`·`[소액]`).

```bash
# 대화형 — y/N 을 묻는다
democut make-cuts --slug demo --items '[…]' --confirmed-total-krw 1200 --rate-version 3

# 비대화형(CI·에이전트) — --yes 가 없으면 거부한다
democut make-cuts … --yes
```

승인 전에는 **요청을 보내지 않는다.** 물어보기만 하고 이미 제출했으면 게이트가 무의미하다.

### 종료 코드

| 코드 | 뜻 | 대응 |
|---|---|---|
| 0 | 성공 | — |
| 1 | 일반 오류 | 미로그인·사용자 취소·알 수 없음 |
| 2 | 고쳐서 재시도 가능 | 인자 오류·승인 누락. **과금 없음** |
| 3 | 일시적 오류 | **그대로 재시도해도 안전하다** |
| 4 | 업스트림 거부 | **재시도하면 재과금될 수 있다 — 자동 재시도 금지** |
| 5 | 대기 시한 초과 | 실패가 아니다. 작업은 서버에서 계속된다 — 재제출 금지 |

2~4 는 서버가 알려 준 재시도 안전성을 그대로 옮긴 것이다. 스크립트가 코드만 보고
「다시 걸어도 되는가」 를 판단할 수 있어야 재제출로 두 번 과금되지 않는다.

### 가이드

```bash
democut guide                 # 서버가 배포하는 에이전트 규약 원문(로그인 불필요)
democut init-agent            # 그 규약을 레포의 CLAUDE.md 에 블록으로 써 준다(멱등)
```

## 개발

```bash
npm install
npm run typecheck
npm test
npm run build      # dist/cli.js (bin: democut) 번들
```

## MCP 서버 (에이전트에 도구로 붙이기)

`democut mcp` 는 **stdio MCP 서버**다. Claude Code·Cursor 등 MCP 클라이언트에 붙이면
에이전트가 도구로 직접 영상을 만든다.

```bash
claude mcp add democut -- npx -y democut@latest mcp
```

`npx` 를 쓰는 이유는 전역 설치가 **연결의 선행 조건이 되지 않게** 하기 위해서다.
설치가 빠지면 등록은 성립하는데(등록은 바이너리 존재를 검사하지 않는다) 연결만 실패하고,
그 실패는 클라이언트의 `Failed to reconnect …` 한 줄로만 드러난다 — 서버가 뜨지 못하므로
진단을 낼 주체가 없다. 자격증명은 패키지가 아니라 `~/.democut/credentials.json` 에 있어
실행 방식을 바꿔도 로그인 상태는 그대로다. 전역 설치는 선택이다.

★ **`@latest` 를 빼지 마라.** `-y` 는 설치 확인 프롬프트를 자동 승인할 뿐 **버전을 다시
해석하지 않는다** — 태그를 빼면 `~/.npm/_npx` 캐시에 남은 옛 버전이 그대로 뜬다
(2026-08-31 실측: 게시본이 0.4.0 인데 캐시는 0.3.0). 그 스큐가 실제로 사고를 냈다. 서버가
`project_slug` 를 필수화한 뒤 게시본이 갱신되기까지 **2시간 45분** 동안 stdio 사용자의 영상
생성이 전부 422 였고, 원격 HTTP 사용자는 아무 영향이 없었다. 대가로 실행할 때마다
레지스트리를 한 번 보므로 npm 에 닿지 못하면 서버가 아예 뜨지 않는데, **조용히 옛 도구를
쓰는 것보다 큰 소리로 안 뜨는 편이 낫다**(레지스트리가 막힌 사내망이면 아래 원격 HTTP 나
전역 설치를 쓴다).

도구 목록은 서버의 계약 표에서 생성된다(`GET /api/meta/tools`) — 수를 여기 적지 않는 이유는 표가 늘면 이 문장이 곧 거짓이 되기 때문이다. 목록과 인자는
`democut --help` 로 보는 것과 같다 — 같은 표에서 나오기 때문이다.

대부분은 조회·견적이라 부작용이 없고, 돈이 나가는 것은 계약이 `charge`·`small` 로 표시한
소수뿐이다(`make_cuts`·`make_export`·`make_thumbnails`·`make_cut_image`·`make_character_photo`·
`generate_youtube_meta`).

### 비용 승인이 프로토콜에 맞게 바뀐다

CLI 는 TTY 에 `y/N` 을 물어 사람을 막지만 MCP 에는 물어볼 화면이 없다. 대신 두 겹이다:

1. **호스트의 도구 승인** — Claude Code 등이 도구 호출 전에 사용자에게 확인을 받는다.
   우리가 제어할 수 없으므로 이것만 믿지 않는다.
2. **`confirmed_total_krw` 재확인** — 생성 도구는 호출자가 *직접 본* 견적 금액과 `rate_version` 을
   다시 실어 보내야 하고, 서버가 방금 계산한 값과 대조해 어긋나면 409 `QUOTE_MISMATCH` 로 거부한다
   (그때 과금은 0이고 새 견적을 준다). 모델이 견적을 건너뛰고 바로 만드는 것을 막고, 사람에게
   보여준 금액과 실제 청구액이 갈라지지 않게 한다.

### stdio 와 원격 HTTP — 어느 쪽을 언제 쓰나

둘은 **대체가 아니라 병존**이고, 도구 이름·인자도 같다. 고르는 기준은 취향이 아니라
**클라이언트가 도는 자리**다.

| | 사람이 쓰는 클라이언트<br>(노트북의 Claude Code·Cursor…) | 헤드리스·자동화 호스트<br>(게이트웨이·CI·서버 박스) |
|---|---|---|
| **권장** | **원격 HTTP** | **stdio (이 CLI)** |
| 로그인 | 붙는 순간 브라우저가 열려 끝난다 | `democut auth login --no-browser` 1회 (device flow, RFC 8628) |
| 브라우저 없는 자리 | **불가** — 비대화형에서는 OAuth 흐름이 안 돈다 | **가능** — 코드를 다른 기기에서 승인 |
| 사전 준비 | 아래 두 옵션을 그대로 복사 | 없음 — redirect URI 등록도 브라우저도 불필요 |
| 버전 스큐 | **없다** — 띄울 로컬 바이너리가 없다 | `@latest` 로 막는다(위 참고) |
| 산출물 | 만료되는 링크로만 받는다 | `--output` 으로 디스크에 저장된다 |

헤드리스에서 갈리는 것은 편의가 아니라 **가능/불가능**이다. 원격 HTTP 의 OAuth 는 호스트가
브라우저를 띄워야 시작되는데, Claude Code 문서가 비대화형에서는 그 흐름을 돌릴 수 없다고
명시한다 — *"In non-interactive mode there's no `/mcp` panel, so Claude Code can't run the
OAuth flow for you."* 사람이 대화형 세션에서 **미리** 인증해 두는 것(`claude mcp login`)이
유일한 우회다. 거기에 Clerk 이 loopback redirect 의 포트를 정확 일치로 요구하므로 그 호스트가
쓸 포트의 redirect URI 도 OAuth 애플리케이션에 미리 등록해야 한다.

stdio 는 device flow(RFC 8628)라 redirect URI 자체가 없고, `--no-browser` 가 코드와 URL 을
출력하면 사람이 **다른 기기에서** 승인한다 — 그래서 브라우저가 없는 자리에서도 성립한다.

```bash
claude mcp add --transport http --client-id 7ovjLP6E85qLNAyq --callback-port 33418 \
    democut https://democut.ai/mcp
```

두 옵션은 **생략할 수 없고 임의로 바꿀 수도 없다.** `--client-id` 는 이 서버가 아무
클라이언트나 받지 않기 때문이고(동적 클라이언트 등록(DCR)을 **일부러 꺼 두었다** —
켜면 아무나 등록해 통과하는 confused-deputy 표면이 열린다), `--callback-port` 는 그 포트의
redirect URI 가 사전 등록돼 있어야 하기 때문이다. 미등록이면 Clerk 이 400
`redirect_uri … does not match … pre-registered redirect urls` 로 거부한다. 위 두 값은
공개값이고 웹 문서(<https://democut.ai/docs/agents>)가 같은 쌍을 안내한다. 설계 근거의
SSOT 는 `apps/api/app/mcp/auth.py` docstring.

> ⚠️ **"클라이언트 설정 파일에 토큰이 남지 않는다" 는 stdio 의 고유 장점이 아니다.**
> 이 문서가 한동안 그렇게 적고 있었는데 **사실이 아니다** — 원격 HTTP 등록도 설정에는
> 공개값 둘(`clientId`·`callbackPort`)만 남고 액세스 토큰은 호스트의 자격증명 저장소에
> 있다(2026-08-31 실측). 두 전송이 같으므로 선택 근거가 되지 못한다. 위 표의 기준으로 골라라.

> ⚠️ 이 문단은 원래 "원격은 동적 클라이언트 등록을 요구해서 체인을 새로 쌓아야 한다" 고
> 적혀 있었는데 **틀렸다.** DCR 은 MCP 명세에서 MUST 가 아니라 SHOULD 이고, 무엇보다 우리
> 인증 공급자(Clerk)가 이미 완전한 OAuth 2.1 인가서버라 **우리가 만든 AS 는 0줄**이다.
> 오류의 모양이 재발 포인트라 남긴다 — *우리 코드*만 재고 조사하고 **이미 구입한 SaaS
> 기능은 세지 않았다.**

## 게시 (npm publish)

**CI 가 자동으로 한다.** 사람이 순서를 기억할 필요가 없다.

```
promote(사람이 누르는 버튼)  →  deploy-dgx  →  npm-publish-cli
```

게시는 **API 배포가 끝난 뒤**다(2026-10-20) — 새 CLI 가 새 요청 필드를 아직 모르는 API 에 닿지 않는다. 배포가 실패하면
게시하지 않는다. 레지스트리의 `latest` 가 이미 더 높은 버전이면(늦게 돈 옛 파이프라인) 게시하지 않는다.

게시는 **trusted publishing(OIDC)** 으로 한다 — 장수명 토큰 없이 CI 가 짧은 수명 자격증명으로
게시한다. npm 이 2027-01 부터 2FA 우회 토큰의 직접 publish 를 막기 때문이고, 그래서 CI 의 node
이미지가 24 다(OIDC 는 npm 11.5.1+ 요구).

⚠️ 게시본에는 **sigstore provenance 서명이 붙지 않는다.** npm 은 소스 저장소가 **공개**일
때만 서명을 받아 주는데(2026-09-03 실측: `422 … Unsupported GitLab CI source repository
visibility: "private"`) 이 레포는 private 이다. CI 설정으로 우회할 수 있는 문제가 아니라
배선을 걷어냈다 — 자세한 사정과 되살리는 절차는 `scripts/ci-npm-publish.sh` 머리말에 있다.

`deploy-dgx` 가 게시 성공을 `needs` 로 요구하므로, **게시가 실패하면 문서가 프로덕션에
나가지 않는다.** `/docs/agents` 가 아직 없는 패키지를 안내하는 창이 구조적으로 열리지 않는다
(무스코프 이름이라 그 창에서 제3자가 선점하면 토큰 탈취로 이어질 수 있다 — CWE-829).

버전이 그대로면 잡은 "이미 게시됨" 으로 즉시 통과한다. 그러니 **게시하려면 `cli/package.json`
의 version 을 올리기만 하면 된다** — `VERSION` ↔ `package.json` 정합은 테스트가 강제하므로
`src/version.ts` 도 같이 올린다.

순서를 강제하는 것은 `.gitlab-ci.yml` 의 `deploy-dgx.needs` 하나뿐이고, 그것이 지워지면
아무 신호 없이 보장이 사라진다 — `scripts/tests/promote.sh` 가 지킨다(`make test-scripts`).

### 선행 설정 (1회)

게시는 **trusted publishing(OIDC)** 으로만 한다 — 장수명 토큰이 없다. npmjs.com 의 `democut` 패키지
설정에서 **신뢰 게시자**를 이 GitLab 프로젝트(namespace/project/CI 파일 경로)로 등록해 두면, 잡의
`id_tokens: NPM_ID_TOKEN`(aud `npm:registry.npmjs.org`)을 npm 이 자동으로 쓴다. GitLab.com 공유 러너에서만
동작하고 npm 11.5.1+ 이 필요하다(잡 이미지 `node:24`).

> `NPM_TOKEN` 변수는 두지 않는다(2026-09-26 폐기). 남아 있어도 게시 스크립트가 쓰지 않고 경고한다.
> 옛 토큰을 정리할 때도 값을 코드·문서·대화·로컬 파일에 옮기지 않는다 — npmjs.com 에서 토큰 자체를 폐기한다.

### 수동 게시 (긴급시)

```bash
cd cli
npm run build
npm publish        # 무스코프는 기본 public — --access 불필요
```

2FA 가 보안 키(WebAuthn)면 `npm publish` 가 브라우저 승인 URL 을 띄운다.
게시되는 것은 `files` 에 적힌 **`dist/` 뿐**이다(소스·테스트 미포함).
`npm pack --dry-run` 으로 목록을 먼저 확인할 수 있다.
