@clipwright/mcp-server 0.1.0 → 0.2.0

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/SKILL.md CHANGED
@@ -1,96 +1,93 @@
1
- # SKILL.md — установка и эксплуатация `clipwright-mcp`
1
+ # SKILL.md — installing and operating `clipwright-mcp`
2
2
 
3
- Самодостаточная инструкция для АГЕНТА (или человека в Claude Code) без устной
4
- помощи от разработчика. Следуй шагам по порядку. Не путать с
5
- `apps/web/.claude/skills/*` — это скиллы дашборда Next.js, к MCP-серверу
6
- отношения не имеют.
3
+ A self-contained guide for an AGENT (or a human in Claude Code) that needs no
4
+ spoken help from a developer. Follow the steps in order. Not to be confused with
5
+ `apps/web/.claude/skills/*` — those are dashboard skills for the Next.js app and
6
+ have nothing to do with the MCP server.
7
7
 
8
- Пакет `@clipwright/mcp-server` (`bin: clipwright-mcp`) опубликован в npm под
9
- лицензией MIT, поэтому репозиторий для установки НЕ НУЖЕНхватит `npx`.
10
- Сборка из исходников осталась ниже как путь для разработки.
8
+ The package `@clipwright/mcp-server` (`bin: clipwright-mcp`) is published to npm
9
+ under the MIT license, so you do NOT need the repository to install it — `npx` is
10
+ enough. Building from source is kept below as the development path.
11
11
 
12
- Три пакета клиентской поверхности — `@clipwright/core`, `@clipwright/sdk`,
13
- `@clipwright/mcp-server` — открыты по MIT; сам сервис (REST API, рендер-пайплайн,
14
- дашборд) закрыт. Это намеренное разделение, а не недосмотр: см. `LICENSE` в
15
- корне репозитория.
12
+ Four client-side packages — `@clipwright/core`, `@clipwright/sdk`,
13
+ `@clipwright/mcp-server` and `@clipwright/cli` are MIT; the service itself
14
+ (REST API, render pipeline, dashboard) is closed. That split is deliberate, not
15
+ an oversight: see `LICENSE` in the repository root.
16
16
 
17
- ## 1. Установка
17
+ ## 1. Installation
18
18
 
19
- ### 1.1 Из npm — обычный путь
19
+ ### 1.1 From npm — the normal path
20
20
 
21
21
  ```bash
22
22
  npx -y -p @clipwright/mcp-server@latest clipwright-mcp
23
23
  ```
24
24
 
25
- Ожидаемо: процесс НЕ завершается, в stderr печатается
26
- `clipwright mcp server running on stdio` и висит, ожидая JSON-RPC. Это и есть
27
- признак исправного сервера, а не зависание. Останови `Ctrl+C`.
25
+ Expected: the process does NOT exit, prints `clipwright mcp server running on
26
+ stdio` to stderr, and hangs waiting for JSON-RPC. That is what a healthy server
27
+ looks like, not a freeze. Stop it with `Ctrl+C`.
28
28
 
29
- Немедленный выход с `CLIPWRIGHT_API_KEY env var is required` тоже ожидаемое
30
- поведение при пустом ключе (`src/index.ts:11-15`), а не поломка сборки.
29
+ Exiting immediately with `CLIPWRIGHT_API_KEY env var is required` is also
30
+ expected when the key is empty (`src/index.ts`) it is not a broken build.
31
31
 
32
- Дальше сразу к разделу 1.3: конфиг хоста для этого варианта отличается только
33
- командой (`npx` вместо `node` с абсолютным путём), оба показаны там.
32
+ Go straight to section 1.3: the host config for this variant differs only in the
33
+ command (`npx` instead of `node` with an absolute path); both are shown there.
34
34
 
35
- ### 1.2 Из исходниковтолько для разработки
35
+ ### 1.2 From sourcedevelopment only
36
36
 
37
- Требования: Node ≥24, pnpm (`packageManager: pnpm@11.13.1` в корневом
38
- `package.json` — при наличии corepack: `corepack enable`).
37
+ Requirements: Node ≥24, pnpm (`packageManager: pnpm@11.13.1` in the root
38
+ `package.json` — with corepack available: `corepack enable`).
39
39
 
40
40
  ```bash
41
- git clone <repo-url> ugc-video-platform # или используй уже готовый чекаут
41
+ git clone <repo-url> ugc-video-platform # or use a checkout you already have
42
42
  cd ugc-video-platform
43
43
  pnpm install
44
- pnpm build # pnpm -r build — собирает ВСЕ пакеты монорепо в топологическом порядке
44
+ pnpm build # pnpm -r build — builds every package in topological order
45
45
  ```
46
46
 
47
- Если нужна только эта поверхность (быстрее, но зависимости `@clipwright/core`
48
- и `@clipwright/sdk` обязаны быть собраны первыми — `...` тянет их
49
- автоматически):
47
+ If you only need this surface (faster, but `@clipwright/core` and
48
+ `@clipwright/sdk` must be built first — `...` pulls them in automatically):
50
49
 
51
50
  ```bash
52
51
  pnpm --filter @clipwright/mcp-server... build
53
52
  ```
54
53
 
55
- После сборки должен существовать файл `packages/mcp-server/dist/index.js`.
56
- Проверь:
54
+ After the build, `packages/mcp-server/dist/index.js` must exist. Check it:
57
55
 
58
56
  ```bash
59
57
  ls packages/mcp-server/dist/index.js
60
58
  ```
61
59
 
62
- #### Предполётная проверка собранного бинаря
60
+ #### Pre-flight check of the built binary
63
61
 
64
- Относится к сборке из исходников. Прежде чем подключать сервер к хосту,
65
- убедись, что он вообще стартует и не падает на импортах:
62
+ This applies to source builds. Before wiring the server into a host, make sure it
63
+ starts at all and does not die on imports:
66
64
 
67
65
  ```bash
68
66
  CLIPWRIGHT_API_KEY=cw_test node packages/mcp-server/dist/index.js
69
67
  ```
70
68
 
71
- Ожидаемо: процесс НЕ завершается сам, в stderr печатается
72
- `clipwright mcp server running on stdio` и висит на stdio (ждёт JSON-RPC).
73
- Останови `Ctrl+C`. Если вместо этого немедленный выход с
74
- `CLIPWRIGHT_API_KEY env var is required` значит переменная не была передана
75
- (это ожидаемое поведение при пустом ключе, см. `src/index.ts:11-15`), не баг.
76
- Если падает на импорте модуля сборка неполная, вернись к шагу 1.2
77
- (`pnpm build` из корня, не адресный `--filter`, чтобы точно пересобрать
78
- `@clipwright/core`/`@clipwright/sdk`).
69
+ Expected: the process does NOT exit on its own, prints `clipwright mcp server
70
+ running on stdio` to stderr, and waits on stdio for JSON-RPC. Stop it with
71
+ `Ctrl+C`. If it instead exits immediately with `CLIPWRIGHT_API_KEY env var is
72
+ required`, the variable was not passed through (expected behaviour on an empty
73
+ key, see `src/index.ts`) — not a bug. If it dies on a module import, the build is
74
+ incomplete: go back to step 1.2 and run `pnpm build` from the root rather than a
75
+ targeted `--filter`, so `@clipwright/core` and `@clipwright/sdk` are definitely
76
+ rebuilt.
79
77
 
80
- ### 1.3 Подключение к MCP-хосту
78
+ ### 1.3 Connecting it to an MCP host
81
79
 
82
- Варианты A и B ниже запускают сервер ИЗ ИСХОДНИКОВ и потому требуют
83
- АБСОЛЮТНЫЙ путь до `dist/index.js`:
80
+ Variants A and B below run the server FROM SOURCE and therefore need the ABSOLUTE
81
+ path to `dist/index.js`:
84
82
 
85
83
  ```bash
86
84
  realpath packages/mcp-server/dist/index.js
87
85
  ```
88
86
 
89
- Если ставил из npm (раздел 1.1), путь не нужен бери вариант В в конце
90
- раздела, он отличается только полями `command`/`args`.
87
+ If you installed from npm (section 1.1), you do not need a path take variant C
88
+ at the end of this section; it differs only in `command`/`args`.
91
89
 
92
- **Вариант A — `.mcp.json` (проектный конфиг хоста, стиль как в
93
- `apps/web/.mcp.json` этого репо):**
90
+ **Variant A — `.mcp.json` (project-level host config):**
94
91
 
95
92
  ```json
96
93
  {
@@ -98,28 +95,29 @@ realpath packages/mcp-server/dist/index.js
98
95
  "clipwright": {
99
96
  "type": "stdio",
100
97
  "command": "node",
101
- "args": ["/абсолютный/путь/до/ugc-video-platform/packages/mcp-server/dist/index.js"],
98
+ "args": ["/absolute/path/to/ugc-video-platform/packages/mcp-server/dist/index.js"],
102
99
  "env": {
103
100
  "CLIPWRIGHT_API_KEY": "cw_...",
104
101
  "CLIPWRIGHT_API_URL": "https://api-production-7e69.up.railway.app",
105
- "CLIPWRIGHT_CLIENT_ID": "<см. раздел 2 — сгенерировать ДО первого запуска>"
102
+ "CLIPWRIGHT_CLIENT_ID": "<see section 2 — generate BEFORE the first run>"
106
103
  }
107
104
  }
108
105
  }
109
106
  }
110
107
  ```
111
108
 
112
- **Вариант B — CLI `claude mcp add`** (эквивалент варианта A одной командой):
109
+ **Variant B — `claude mcp add`** (variant A as a single command):
113
110
 
114
111
  ```bash
115
112
  claude mcp add clipwright \
116
113
  -e CLIPWRIGHT_API_KEY=cw_... \
117
114
  -e CLIPWRIGHT_API_URL=https://api-production-7e69.up.railway.app \
118
- -e CLIPWRIGHT_CLIENT_ID="$(uuidgen | tr -d '-')" \
119
- -- node /абсолютный/путь/до/ugc-video-platform/packages/mcp-server/dist/index.js
115
+ -e CLIPWRIGHT_CLIENT_ID="$(node -e "console.log(crypto.randomUUID().replaceAll('-',''))")" \
116
+ -- node /absolute/path/to/ugc-video-platform/packages/mcp-server/dist/index.js
120
117
  ```
121
118
 
122
- **Вариант Виз npm, без репозитория.** То же самое, но команда `npx`:
119
+ **Variant Cfrom npm, without the repository.** Same thing, but the command is
120
+ `npx`:
123
121
 
124
122
  ```json
125
123
  {
@@ -131,171 +129,193 @@ claude mcp add clipwright \
131
129
  "env": {
132
130
  "CLIPWRIGHT_API_KEY": "cw_...",
133
131
  "CLIPWRIGHT_API_URL": "https://api-production-7e69.up.railway.app",
134
- "CLIPWRIGHT_CLIENT_ID": "<см. раздел 2 — сгенерировать ДО первого запуска>"
132
+ "CLIPWRIGHT_CLIENT_ID": "<see section 2 — generate BEFORE the first run>"
135
133
  }
136
134
  }
137
135
  }
138
136
  }
139
137
  ```
140
138
 
141
- Одной командой:
139
+ As a single command:
142
140
 
143
141
  ```bash
144
142
  claude mcp add clipwright \
145
143
  -e CLIPWRIGHT_API_KEY=cw_... \
146
144
  -e CLIPWRIGHT_API_URL=https://api-production-7e69.up.railway.app \
147
- -e CLIPWRIGHT_CLIENT_ID="$(uuidgen | tr -d '-')" \
145
+ -e CLIPWRIGHT_CLIENT_ID="$(node -e "console.log(crypto.randomUUID().replaceAll('-',''))")" \
148
146
  -- npx -y -p @clipwright/mcp-server@latest clipwright-mcp
149
147
  ```
150
148
 
151
- **`@latest` здесь осознанный выбор, а не небрежность.** Он тянет свежую
152
- версию при каждом запуске: для прототипа это правильно, потому что контракт
153
- ещё меняется и рассинхрон клиента с сервером дороже, чем неожиданное
154
- обновление. Когда контракт устоится, версию надо будет прибить.
149
+ **`@latest` here is a deliberate choice, not sloppiness.** It pulls a fresh
150
+ version on every start: right for a prototype, because the contract still moves
151
+ and a client out of sync with the server costs more than an unexpected upgrade.
152
+ Once the contract settles, the version should be pinned.
155
153
 
156
- Переменные окружения:
154
+ Environment variables:
157
155
 
158
- | Переменная | Обязательна | Значение |
156
+ | Variable | Required | Meaning |
159
157
  |---|---|---|
160
- | `CLIPWRIGHT_API_KEY` | ДА | Bearer-токен формата `cw_*`. Без него процесс завершается сразу с ошибкой (`src/index.ts:11-15`). Токен ВЫДАН ЛИЧНО ТЕБЕ (US-550): он лежит в таблице `api_tokens`, привязан к аккаунту и отзывается отдельно от чужих. Не пересылай его дальшевторому человеку выпускается свой. Разным аккаунтам не видны раны друг друга, и суточный потолок трат у каждого свой. |
161
- | `CLIPWRIGHT_API_URL` | НЕТ | Прод-URL. Если не задать SDK сам подставляет `https://api-production-7e69.up.railway.app` (дефолт в `packages/sdk/src/index.ts:18`). Указывай явно только если целишься в другое окружение. |
162
- | `CLIPWRIGHT_CLIENT_ID` | ДА (см. раздел 2) | Уникальный идентификатор УСТАНОВКИ. |
163
-
164
- После подключения перезапусти/переоткрой MCP-хост и убедись, что `tools/list`
165
- отдаёт четыре тула: `make_ugc`, `get_run`, `quote_ugc`, `list_voices`.
166
-
167
- ## 2. `CLIPWRIGHT_CLIENT_ID` — обязательно и ПОЧЕМУ
168
-
169
- **Правило: КАЖДАЯ установка обязана получить СВОЙ уникальный
170
- `CLIPWRIGHT_CLIENT_ID` ДО первого вызова `make_ugc`.** Не копировать значение
171
- с другой машины, не оставлять пустым в расчёте на дефолт "как-нибудь
172
- сработает".
173
-
174
- ### Почему это не опционально
175
-
176
- Ключ идемпотентности, которым SDK помечает каждый платный `POST
177
- /v1/skills/make_ugc/run`, детерминированно вычисляется из **хеша входа
178
- (`script`/`person`/`image`/… после canonicalize) И идентичности клиента**
179
- (`idempotencyKeyFor`, `packages/core/src/idempotency.ts`; используется в
180
- `packages/sdk/src/index.ts:104-115`). Сервер дедуплицирует по этому ключу:
181
- `apps/api/src/app.ts:103` если для уже сохранённого ключа
182
- `existing.request_hash === hash`, он возвращает 200 с раном, который
183
- **создал первый запрос с этим ключом**, а не запускает новый платный рендер.
184
-
185
- Это правильное поведение для ретрая ОДНОГО И ТОГО ЖЕ пользователя (не платить
186
- дважды за случайный дубль-клик агента). Но:
187
-
188
- - поиск по ключу идемпотентности идёт по ключу, а не по аккаунту
189
- (`runs_idempotency_key_unique` частичный индекс по одной колонке);
190
- - поэтому **разделение аккаунтов токеном (US-550) само по себе НЕ разводит
191
- одинаковые входы**: `clientId`, примешанный в хеш ключа, остаётся тем, что
192
- делает ключи разными;
193
- - и он же нужен внутри одного аккаунта: ноутбук и CI одного человека обязаны
194
- ретраить независимо.
195
-
196
- Если второй пользователь не выставил СВОЙ `CLIPWRIGHT_CLIENT_ID` и его запрос
197
- (тот же скрипт, те же опции например, оба используют пример из этого
198
- SKILL.md дословно) совпадает по входу с чужим уже выполненным раном, сервер
199
- схлопнёт их в ОДИН ран. Второй пользователь получит 200, `run_id` и играющуюся
200
- ссылку но это будет ЧУЖОЕ видео, представленное ему как его собственный
201
- успех. Это дефект 3.2 плана Этапа 5: диагностический инструмент/установка
202
- прочитались бы как «работает», хотя платный рендер для второго пользователя
203
- никогда не запускался.
204
-
205
- ### Как выставить
206
-
207
- Явный `CLIPWRIGHT_CLIENT_ID` в env хоста (раздел 1.3, вариант A/B) самый
208
- надёжный путь, не зависящий от состояния файловой системы. Значение любая
209
- уникальная строка; для согласованности с генератором SDK используй тот же
210
- формат (UUID без дефисов):
158
+ | `CLIPWRIGHT_API_KEY` | YES | Bearer token shaped `cw_*`. Without it the process exits immediately with an error (`src/index.ts`). The token is ISSUED TO YOU PERSONALLY: it lives in the `api_tokens` table, is bound to an account, and is revoked independently of anyone else's. Do not forward it a second person gets their own. What separates people is the ACCOUNT, not the token: accounts cannot see each other's runs, each has its own daily cap and its own idempotency axis, and several tokens on one account share all of it. |
159
+ | `CLIPWRIGHT_API_URL` | no | Production URL. Left unset, the SDK supplies `https://api-production-7e69.up.railway.app` itself. Set it explicitly only when you target another environment. |
160
+ | `CLIPWRIGHT_CLIENT_ID` | YES (see section 2) | Unique identifier of this INSTALLATION. |
161
+
162
+ After connecting, restart or reopen the MCP host and confirm that `tools/list`
163
+ returns four tools: `make_ugc`, `get_run`, `quote_ugc`, `list_voices`.
164
+
165
+ ## 2. `CLIPWRIGHT_CLIENT_ID` — required, and WHY
166
+
167
+ **Rule: EVERY installation must get its OWN unique `CLIPWRIGHT_CLIENT_ID` BEFORE
168
+ the first `make_ugc` call.** Do not copy the value from another machine, and do
169
+ not leave it empty hoping the default will "work out somehow".
170
+
171
+ ### Why this is not optional
172
+
173
+ The idempotency key the SDK stamps on every paid `POST /v1/skills/make_ugc/run`
174
+ is derived deterministically from **the hash of the input** (`script`, `person`,
175
+ `image`, after canonicalisation) **and the client identity**
176
+ (`idempotencyKeyFor` in `packages/core/src/idempotency.ts`, used by
177
+ `packages/sdk/src/index.ts`). The server deduplicates on that key: if a stored
178
+ key matches on `request_hash`, it returns 200 with the run **created by the first
179
+ request that used the key**, instead of starting a new paid render.
180
+
181
+ That is the right behaviour when the SAME user retries you should not pay twice
182
+ for an agent's accidental double click. Its boundaries are these.
183
+
184
+ Collapsing requires BOTH to match: the account and the `clientId`. Separating
185
+ either one is enough.
186
+
187
+ - **The account separates by construction.** Both the unique index
188
+ (`runs_account_idempotency_key_unique`) and the lookup use the pair
189
+ `(account_id, idempotency_key)`. A run from ANOTHER account can never reach you,
190
+ and nobody can take your key — whatever sits in `CLIPWRIGHT_CLIENT_ID`.
191
+ **The axis is the account, not the token.** A separate token does not by itself
192
+ mean a separate account: several live tokens on one account are normal (laptop,
193
+ CI, smoke script), and they all share one idempotency axis. The account is not
194
+ visible from the token only the service side knows it, so "is this my own
195
+ account?" is confirmed by whoever issued the token.
196
+ - **`clientId` separates within one account.** On its own it comes from
197
+ `~/.clipwright/client-id` and is born random on first run, so two genuinely
198
+ separate installations diverge even without the environment variable. It can
199
+ only match if it was MADE to match: a shared `HOME`, a value copied from
200
+ someone else's machine, an image cloned together with the file.
201
+
202
+ The dangerous case is exactly the crossing of those two conditions: **one account
203
+ AND one `clientId`**. Then a request whose input matches an earlier one — for
204
+ example, both people copied a sample from this SKILL.md verbatim — returns 200, a
205
+ `run_id` and a playable link. But it is the video rendered for the first person,
206
+ presented to the second as their own success. A diagnostic tool would read as
207
+ "works" while no paid render ever ran for the second person.
208
+
209
+ Hence the rule: **a second person gets their own ACCOUNT and a token issued on
210
+ it**, and the value of `CLIPWRIGHT_CLIENT_ID` is not copied from anyone's machine.
211
+ Setting it explicitly is mandatory wherever the file resolver cannot give a stable
212
+ value of its own: a read-only or shared `HOME`, a container from a cloned image,
213
+ CI.
214
+
215
+ ### How to set it
216
+
217
+ An explicit `CLIPWRIGHT_CLIENT_ID` in the host environment (section 1.3, variants
218
+ A/B/C) is the most reliable path, because it does not depend on the state of the
219
+ file system. The value is any unique string; for consistency with the SDK's own
220
+ generator use the same shape (a UUID without dashes). Generate it with Node, not
221
+ `uuidgen`: Node is certainly present here, whereas `uuidgen` may be missing on a
222
+ bare Linux image — and then the shell silently substitutes an EMPTY string, which
223
+ switches off precisely the protection the variable exists for.
211
224
 
212
225
  ```bash
213
- uuidgen | tr -d '-'
226
+ node -e "console.log(crypto.randomUUID().replaceAll('-',''))"
214
227
  ```
215
228
 
216
- Если переменная НЕ задана, `resolveClientId()`
217
- (`packages/sdk/src/client-id.ts`) падает на файл `~/.clipwright/client-id`:
218
- при первом запуске генерирует случайный id и сохраняет его туда, дальше читает
219
- оттуда. Это рабочий фолбэк для одиночной установки на чистой машине, но НЕ
220
- полагайся на него как на единственную защиту при установке агентом: домашние
221
- каталоги при клонировании образов/контейнеров/дев-окружений могут унести файл
222
- `~/.clipwright/client-id` с собой и породить коллизию, которую этот фолбэк
223
- не заметит. Для установки по этой инструкции **всегда выставляй
224
- `CLIPWRIGHT_CLIENT_ID` явно в env хоста**, не полагайся на автогенерацию.
225
-
226
- Если ни env, ни запись файла невозможны (read-only `$HOME`), `resolveClientId()`
227
- намеренно БРОСАЕТ исключение вместо тихого эфемерного id на каждый запуск —
228
- молчаливый фолбэк убил бы дедупликацию целиком (`packages/sdk/src/client-id.ts:9-19`).
229
- Если сервер упал с этой ошибкой — это сигнал поставить `CLIPWRIGHT_CLIENT_ID`
230
- явно, а не искать баг.
231
-
232
- ## 3. ПОРЯДОК ВЫЗОВОВ сначала `quote_ugc`, потом `make_ugc`
233
-
234
- Это не совет по вежливости, а механизм, без которого контракт молчит о своих
235
- решениях.
229
+ If the variable is NOT set, `resolveClientId()` (`packages/sdk/src/client-id.ts`)
230
+ falls back to the file `~/.clipwright/client-id`: on first run it generates a
231
+ random id and stores it there, then reads it back. That is a workable fallback for
232
+ a single installation on a clean machine, but do NOT lean on it as your only
233
+ protection when an agent does the install: home directories carried along when
234
+ images, containers or dev environments are cloned can take
235
+ `~/.clipwright/client-id` with them and produce a collision this fallback will not
236
+ notice. When installing by this guide, **always set `CLIPWRIGHT_CLIENT_ID`
237
+ explicitly in the host environment** rather than relying on auto-generation.
238
+
239
+ If neither the variable nor writing the file is possible (read-only `$HOME`),
240
+ `resolveClientId()` deliberately THROWS instead of inventing a throwaway id on
241
+ every start a silent fallback would disable deduplication entirely
242
+ (`packages/sdk/src/client-id.ts`).
243
+
244
+ **The server does NOT die in that case, and this is verified by a test run.** The
245
+ identity resolves lazilyonly on the first `make_ugc`. In an environment where
246
+ `~/.clipwright` is not writable the server starts, `tools/list` returns all four
247
+ tools, and `quote_ugc`, `get_run` and `list_voices` work: they do not need the
248
+ identity. Only `make_ugc` refuses, and its refusal names the variable that fixes
249
+ it. So the action to take is to set `CLIPWRIGHT_CLIENT_ID` explicitly — not to
250
+ hunt for a bug and not to reinstall the package.
251
+
252
+ ## 3. CALL ORDER — `quote_ugc` first, `make_ugc` second
253
+
254
+ This is not a politeness convention but a mechanism, without which the contract
255
+ stays silent about its own decisions.
236
256
 
237
257
  ```
238
- quote_ugc → прочитать source / resolved_aspect_ratio / warnings → make_ugc → get_run (поллинг)
258
+ quote_ugc → read source / resolved_aspect_ratio / warnings → make_ugc → get_run (polling)
239
259
  ```
240
260
 
241
- **Что даёт `quote_ugc`, чего не даёт ничего другого.** Он бесплатен, ничего не
242
- резервирует и отвечает на вопросы, которые иначе выяснятся уже после списания:
261
+ **What `quote_ugc` gives you that nothing else does.** It is free, reserves
262
+ nothing, and answers the questions that would otherwise surface only after money
263
+ is spent:
243
264
 
244
- | Поле ответа | Что из него видно |
265
+ | Response field | What it tells you |
245
266
  |---|---|
246
- | `credits_estimate` | ценапокажи её пользователю ДО генерации |
247
- | `source` | размеры источника кадра: пробленный `image` либо дефолтный актёр (432×768) |
248
- | `resolved_aspect_ratio` | формат, который РЕАЛЬНО будет отрендерен на этом входе |
249
- | `warnings[]` | какие переданные параметры не будут соблюдены |
250
- | `contract_version` | под какой версией контракта получена эта цена |
267
+ | `credits_estimate` | the price show it to the user BEFORE generating |
268
+ | `source` | dimensions of the source frame: a probed `image`, or the default actor (432×768) |
269
+ | `resolved_aspect_ratio` | the aspect ratio that will ACTUALLY be rendered for this input |
270
+ | `warnings[]` | which of the parameters you passed will not be honoured |
271
+ | `contract_version` | which contract version this price was quoted under |
251
272
 
252
- **Вердикт `quote_ugc` и вердикт `make_ugc` совпадают по построению** — обе
253
- поверхности зовут одну функцию. Если quote вернул 400, ран вернёт тот же 400;
254
- если quote показал `resolved_aspect_ratio: "9:16"` на запрошенном `1:1`, то
255
- именно 9:16 и отрендерится.
273
+ **The verdict of `quote_ugc` and the verdict of `make_ugc` agree by
274
+ construction** both surfaces call the same function. If quote returns 400, the
275
+ run returns the same 400; if quote showed `resolved_aspect_ratio: "9:16"` for a
276
+ requested `1:1`, then 9:16 is what gets rendered.
256
277
 
257
- **Отклонённые поляне пытайся их передавать.** Они не объявлены в схеме тула,
258
- и сервер отвечает 400 `rejected_field` ещё до создания рана:
278
+ **Rejected fieldsdo not try to pass them.** They are not declared in the tool
279
+ schema, and the server answers 400 `rejected_field` before a run is even created:
259
280
 
260
- | Поле | Чем заменить |
281
+ | Field | Use instead |
261
282
  |---|---|
262
- | `character` | `person` — описание актёра словами, или ничего (дефолтный актёр) |
263
- | `webhook_url` | поллинг `get_run` — вебхуки не доставляются вообще |
264
- | `segments`, `broll_url` | одним `script`; сегментные ролики ещё не доступны |
265
- | `disclosure_overlay` | ничем: маркировка уже есть в ответе рана и в самом файле |
266
- | `image` при выключенном килсвитче | опуститьрендер пойдёт на дефолтном актёре |
283
+ | `character` | `person` — describe the actor in words, or nothing (default actor) |
284
+ | `webhook_url` | poll `get_run` — webhooks are not delivered at all |
285
+ | `segments`, `broll_url` | a single `script`; segmented clips are not available yet |
286
+ | `disclosure_overlay` | nothing: the AI disclosure is already in the run response and in the file |
287
+ | `image` with the killswitch off | omit it the render runs on the default actor |
267
288
 
268
- **Формат может быть отклонён.** Запрос `aspect_ratio: "1:1"` без `image`
269
- вернёт отказ: дефолтный актёр вертикальный (432×768), и квадратный кадр из
270
- него получился бы только центр-кропом вендора, чего мы не делаем молча. Чтобы
271
- получить `1:1`, передай `image` с квадратным публичным https-источником.
289
+ **The aspect ratio can be refused.** Requesting `aspect_ratio: "1:1"` without an
290
+ `image` returns a refusal: the default actor is vertical (432×768), and a square
291
+ frame could only come from the vendor's centre crop, which we do not do silently.
292
+ To get `1:1`, pass an `image` with a square public https source.
272
293
 
273
- ## 4. Четыре тула
294
+ ## 4. The four tools
274
295
 
275
- Схемы единый источник правды `@clipwright/core`
276
- (`packages/core/src/skills.ts`, барррель `packages/core/src/index.ts`), MCP их
277
- не переписывает руками (`packages/mcp-server/src/index.ts:6-8`).
296
+ Schemas come from the single source of truth `@clipwright/core`
297
+ (`packages/core/src/skills.ts`, barrel `packages/core/src/index.ts`); MCP does not
298
+ retype them by hand.
278
299
 
279
- ### `quote_ugc` — бесплатно
300
+ ### `quote_ugc` — free
280
301
 
281
- Вход `offeredUgcInputShape` (`packages/mcp-server/src/server.ts`), то есть
282
- контракт МИНУС отклонённые поля: их там просто нет, и перечислять их здесь
283
- значило бы предлагать то, за что сервер накажет (раздел 3).
302
+ The input is `offeredUgcInputShape` (`packages/mcp-server/src/server.ts`), that
303
+ is, the contract MINUS the rejected fields: they simply are not there, and listing
304
+ them here would mean offering what the server punishes you for (section 3).
284
305
 
285
- `script` (≤10000 симв., счёт слов ≤`MAX_SCRIPT_WORDS` ≈45 с речи; формально
286
- `.optional()`, но обязателен, пока `segments` отклонён), опционально `person`
287
- ИЛИ `image` (публичный `https`-URL), плюс `name`, `captions` (по умолчанию
288
- `false` **субтитры opt-in, СНАЧАЛА спроси пользователя**), `caption_style`
306
+ `script` (≤10000 chars, word count ≤`MAX_SCRIPT_WORDS` ≈45 s of speech; formally
307
+ `.optional()`, but required while `segments` is rejected), optionally `person` OR
308
+ `image` (a public `https` URL), plus `name`, `captions` (defaults to `false` —
309
+ **captions are opt-in, ASK THE USER FIRST**), `caption_style`
289
310
  (`hormozi`/`tiktok`/`minimal`), `look` (`natural`/`commercial`/`raw_iphone`),
290
- `aspect_ratio` (`9:16`/`1:1`, БЕЗ дефолтамолчание и явный выбор различаются),
291
- `resolution` (`720p`/`1080p`/`4k`), `voice`/`voice_id`.
311
+ `aspect_ratio` (`9:16`/`1:1`, with NO default silence and an explicit choice
312
+ differ), `resolution` (`720p`/`1080p`/`4k`), `voice`/`voice_id`.
292
313
 
293
- Точный список всегда доступен из самой схемы тула: `tools/list` порождается из
294
- того же shape, и поля, которые не соблюдаются, несут `NOT HONORED YET` прямо в
295
- своём `description`.
314
+ The exact list is always available from the tool schema itself: `tools/list` is
315
+ generated from the same shape, and fields that are not honoured carry
316
+ `NOT HONORED YET` right in their own `description`.
296
317
 
297
- Не тратит кредиты. Ответ необработанный `quote` целиком
298
- (`packages/mcp-server/src/server.ts`):
318
+ Spends no credits. The response is the raw `quote` in full:
299
319
 
300
320
  ```json
301
321
  {
@@ -306,20 +326,19 @@ quote_ugc → прочитать source / resolved_aspect_ratio / warnings
306
326
  }
307
327
  ```
308
328
 
309
- `warnings[]` здесь ПРИСУТСТВУЕТ (передаётся без фильтрации) — читай его:
310
- несоблюдённые параметры (например, `captions` проигнорирован в этой сборке)
311
- объявляются сюда, а не молча теряются.
329
+ `warnings[]` IS present here (passed through unfiltered) — read it: parameters
330
+ that will not be honoured (for example, `captions` ignored in this build) are
331
+ declared there rather than dropped silently.
312
332
 
313
- **Всегда вызывай `quote_ugc` первым и покажи пользователю цену до `make_ugc`**
314
- (императив в описании тула, `packages/mcp-server/src/index.ts:83-85`).
333
+ **Always call `quote_ugc` first and show the user the price before `make_ugc`**
334
+ (the tool description says so in the imperative).
315
335
 
316
- ### `make_ugc` — платный, создаёт ран
336
+ ### `make_ugc` — paid, creates a run
317
337
 
318
- Вход тот же `makeUgcInputShape` + `attempt` (целое ≥1, опционален; см.
319
- раздел 4). Денежный контур: это ОДИН платный рендер за вызов (если не сработал
320
- дедуп идемпотентности — раздел 4). **Тул НЕ ждёт готовности видео**он
321
- стартует ран и немедленно возвращает `run_id`
322
- (`packages/mcp-server/src/index.ts:28-29,39-40`):
338
+ The input is the same shape plus `attempt` (an integer ≥1, optional; see section
339
+ 5). Money: this is ONE paid render per call, unless idempotency deduplication kicks
340
+ in (section 2). **The tool does NOT wait for the video** it starts the run and
341
+ returns a `run_id` immediately:
323
342
 
324
343
  ```json
325
344
  {
@@ -331,36 +350,35 @@ quote_ugc → прочитать source / resolved_aspect_ratio / warnings
331
350
  }
332
351
  ```
333
352
 
334
- Дальше ОБЯЗАТЕЛЬНО поллить `get_run` этим `run_id` каждые ~5–10 секунд до
335
- терминала. Не сообщай пользователю, что видео готово, пока не получил
353
+ After that you MUST poll `get_run` with that `run_id` every ~5–10 seconds until it
354
+ reaches a terminal state. Do not tell the user the video is ready until you have a
336
355
  `video_url`.
337
356
 
338
- #### Выбор голоса (`voice` / `voice_id`)
339
-
340
- Два взаимоисключающих опциональных поля во входе `make_ugc`:
357
+ #### Choosing a voice (`voice` / `voice_id`)
341
358
 
342
- - `voice` имя **курируемого пресета**. **Рекомендуемый путь.** Список — тул
343
- `list_voices` (бесплатный): `owner_ru_clone`, `sarah`, `george`, `eric`,
344
- `daria_ru_female`. Валидируется на границе: неизвестное имя → 400 сразу,
345
- платный вызов не создаётся.
346
- - `voice_id` — **сырой ElevenLabs id** (20-символьный), escape hatch для
347
- голосов ВНЕ каталога, включая клонированные. Проверяется бесплатно против
348
- аккаунта в начале рана (не на `quote`): несуществующий id → ран `failed` ДО
349
- единого платного вызова.
350
- - **Ни одного не задавай без просьбы пользователя.** Голос по умолчанию уже
351
- подобран под текущий аватар; менять его не нужно. `list_voices` — курируемый
352
- каталог по дизайну, не полный список голосов вендора.
359
+ Two mutually exclusive optional fields in the `make_ugc` input:
353
360
 
354
- Детали и инвариант «неверный голос не стоит денег» `docs/16-voice-selection.md`.
361
+ - `voice` the name of a **curated preset**. **The recommended path.** The list
362
+ comes from the free `list_voices` tool: `owner_ru_clone`, `sarah`, `george`,
363
+ `eric`, `daria_ru_female`. Validated at the boundary: an unknown name returns
364
+ 400 immediately and no paid call is made.
365
+ - `voice_id` — a **raw ElevenLabs id** (20 characters), an escape hatch for voices
366
+ OUTSIDE the catalog, cloned ones included. It is checked for free against the
367
+ account at the start of the run (not during `quote`): a non-existent id fails the
368
+ run BEFORE any paid call.
369
+ - **Set neither unless the user asks.** The default voice is already matched to
370
+ the current avatar; there is no need to change it. `list_voices` is a curated
371
+ catalog by design, not the vendor's full voice list.
355
372
 
356
- ### `get_run` — поллинг статуса
373
+ ### `get_run` — status polling
357
374
 
358
- Вход: `{ "run_id": "run_..." }`. Форма ответа реализована в
359
- `packages/mcp-server/src/format.ts` (`formatGetRun`) ниже она приведена
360
- ФАКТИЧЕСКИ, после исправления US-501.
375
+ Input: `{ "run_id": "run_..." }`. The response shape is implemented in
376
+ `packages/mcp-server/src/format.ts` (`formatGetRun`) and is given below as it
377
+ actually is.
361
378
 
362
- **Нетерминальный ран** (`queued`/`scripting`/`tts`/`avatar`/`compositing`/`uploading`)
363
- несёт `state` стадии, `status` всегда `"IN_PROGRESS"`:
379
+ **A non-terminal run**
380
+ (`queued`/`scripting`/`tts`/`avatar`/`compositing`/`uploading`) carries the stage
381
+ in `state`, with `status` always `"IN_PROGRESS"`:
364
382
 
365
383
  ```json
366
384
  {
@@ -372,9 +390,7 @@ quote_ugc → прочитать source / resolved_aspect_ratio / warnings
372
390
  }
373
391
  ```
374
392
 
375
- **Терминальный успех** несёт И `status: "SUCCEEDED"`, И `state: "succeeded"`
376
- (до US-501 `state` в терминальной успешной ветке отсутствовал — грабля №4
377
- `docs/14-gate4-heygen.md:75-79` — теперь это исправлено, поле есть всегда):
393
+ **Terminal success** carries BOTH `status: "SUCCEEDED"` AND `state: "succeeded"`:
378
394
 
379
395
  ```json
380
396
  {
@@ -386,155 +402,256 @@ quote_ugc → прочитать source / resolved_aspect_ratio / warnings
386
402
  }
387
403
  ```
388
404
 
389
- **Терминальный отказ** несёт И `status: "FAILED"`, И `state: "failed"`:
405
+ **Terminal failure** carries BOTH `status: "FAILED"` AND `state: "failed"`:
390
406
 
391
407
  ```json
392
408
  {
393
409
  "status": "FAILED",
394
410
  "run_id": "run_...",
395
411
  "state": "failed",
396
- "error": "<текст ошибки>"
412
+ "error": "<error text>"
397
413
  }
398
414
  ```
399
415
 
400
- **`warnings[]` в `get_run`:** каждая ветка ответа (`IN_PROGRESS`, `SUCCEEDED`,
401
- `FAILED`) несёт `warnings` рана (US-512; правило проекта №3 — несоблюдённые
402
- параметры объявляются, никогда молча). Читай его на терминале обязательно:
403
- например, отсутствие субтитров в прототипе объявляется именно здесь.
416
+ **`warnings[]` in `get_run`:** every branch of the response (`IN_PROGRESS`,
417
+ `SUCCEEDED`, `FAILED`) carries the run's `warnings` parameters that were not
418
+ honoured are always declared, never dropped silently. Read it on the terminal
419
+ response without fail: for instance, missing captions in the prototype are
420
+ announced exactly there.
404
421
 
405
- **Неизвестный терминальный статус на сервере** (например гипотетический
406
- `"canceled"`, не входящий в `TERMINAL_STATES`) осознанная деградация:
407
- `isTerminal` вернёт `false`, и такой ран попадёт в ветку `IN_PROGRESS` (агенту
408
- скажут «продолжай поллить» про уже завершённый ран). Это принятое поведение
409
- прототипа (MCP и API деплоятся одной версией), не баг форматтера.
422
+ **An unknown terminal status on the server** (say a hypothetical `"canceled"` that
423
+ is not in `TERMINAL_STATES`) is a deliberate degradation: `isTerminal` returns
424
+ `false`, so such a run lands in the `IN_PROGRESS` branch and the agent is told to
425
+ keep polling a run that has already finished. That is accepted prototype behaviour
426
+ MCP and the API ship as one version not a formatter bug.
410
427
 
411
- ### `list_voices` — бесплатно, без входа
428
+ ### `list_voices` — free, no input
412
429
 
413
- Отдаёт курируемый каталог пресетов для поля `voice` у `make_ugc`
414
- (`packages/mcp-server/src/server.ts`, регистрация четвёртым; `inputSchema: {}` —
415
- аргументов нет). Денег не стоит и рана не создаёт.
430
+ Returns the curated preset catalog for the `voice` field of `make_ugc`
431
+ (`inputSchema: {}` — it takes no arguments). Costs nothing and creates no run.
416
432
 
417
- Сетевой вызов при этом есть: тула ходит в НАШ `/v1/voices` и требует рабочий
418
- `CLIPWRIGHT_API_KEY`, как и остальные три. К вендору не ходит никто список
419
- статический и живёт в `packages/core/src/voices.ts`, поэтому вендорский ключ на
420
- этой поверхности не нужен.
433
+ There is still a network call: the tool hits OUR `/v1/voices` and needs a working
434
+ `CLIPWRIGHT_API_KEY`, like the other three. Nobody calls the vendorthe list is
435
+ static and lives in `packages/core/src/voices.ts`, so no vendor key is needed on
436
+ this surface.
421
437
 
422
- Зови его до `make_ugc`, если пользователь просит «другой голос»: имя пресета
423
- предпочтительнее сырого `voice_id`, потому что пресет переживает смену вендора,
424
- а идентификатор нет (раздел про `voice`/`voice_id` выше).
438
+ Call it before `make_ugc` when the user asks for "a different voice": a preset
439
+ name is preferable to a raw `voice_id`, because a preset survives a change of
440
+ vendor and an identifier does not.
425
441
 
426
- ## 5. Повтор упавшего рана это НЕ перманентный отказ
442
+ ## 5. Retrying a failed run is NOT a permanent refusal
427
443
 
428
- Если `get_run` вернул терминальный `FAILED` и ты вызываешь `make_ugc` СНОВА с
429
- ТЕМ ЖЕ входом (тот же `script`/опции) и БЕЗ изменения `attempt` сервер
430
- вернёт ТОТ ЖЕ терминальный `failed`-ран как есть (та же ошибка, тот же
431
- `run_id`), а НЕ запустит новый платный рендер. Это дедупликация по ключу
432
- идемпотентности (раздел 2), а не признак того, что видео навсегда
433
- недостижимо.
444
+ If `get_run` returned a terminal `FAILED` and you call `make_ugc` AGAIN with the
445
+ SAME input (same `script` and options) and WITHOUT changing `attempt`, the server
446
+ returns THAT SAME terminal failed run as it is (same error, same `run_id`) instead
447
+ of starting a new paid render. That is idempotency deduplication (section 2), not
448
+ a sign that the video is forever out of reach.
434
449
 
435
- **Чтобы реально повторить попытку, передай `attempt` со значением на единицу
436
- больше предыдущего** (по умолчанию первый вызов имеет `attempt=1`; для второй
437
- попытки `attempt: 2`, для третьей `attempt: 3`, и т.д.). Это дословно
438
- поведение, задокументированное в описании тула `make_ugc`
439
- (`packages/mcp-server/src/index.ts:29-30`):
450
+ **To actually retry, pass `attempt` one higher than before** (the first call is
451
+ `attempt=1` by default; the second attempt is `attempt: 2`, the third `attempt: 3`,
452
+ and so on). This is the behaviour documented verbatim in the `make_ugc` tool
453
+ description:
440
454
 
441
455
  > "Pass attempt=2,3,… to deliberately start a NEW run for the same input
442
456
  > (retry after a failure)."
443
457
 
444
- Технически `attempt` уходит не в тело рендер-запроса, а в опции SDK
445
- (`packages/mcp-server/src/index.ts:35-40`) и превращается в суффикс
446
- `:N` у ключа идемпотентности (`packages/sdk/src/index.ts:104-115`) другой
447
- суффикс даёт другой ключ сервер видит НОВЫЙ запрос → новый `run_id` → новый
448
- платный вызов вендора.
458
+ Technically `attempt` does not go into the render request body but into the SDK
459
+ options, and becomes a `:N` suffix on the idempotency key — a different suffix
460
+ gives a different key, so the server sees a NEW request, which means a new
461
+ `run_id` and a new paid vendor call.
462
+
463
+ Do not confuse this with `CLIPWRIGHT_CLIENT_ID`: `attempt` is the agent's
464
+ deliberate "this is a new attempt", while `clientId` is the fixed identity of the
465
+ installation and must NOT change between attempts.
466
+
467
+ ### ⚠️ EVERY `attempt` BUMP COSTS MONEY
468
+
469
+ A new `attempt` is a new run and a **new paid vendor call**, not a free resend.
470
+ The loop "failed → bump → failed → bump" burns real credits on every iteration.
471
+
472
+ So:
473
+
474
+ - **read the `error` of the failed run before retrying.** Refusals such as
475
+ `rejected_field`, `aspect_conflict` or `daily_cap_exceeded` are NOT fixed by
476
+ retrying — they are determined by the input or by a cap, and the next attempt
477
+ fails the same way, only now for money;
478
+ - **bump `attempt` only for a refusal that looks temporary** (vendor timeout, 5xx,
479
+ a dropped connection);
480
+ - **the cap exists and it is daily.** Exhausting it gives you
481
+ `daily_cap_exceeded` — that is a protection doing its job, not a broken product.
482
+ Wait for the next day or ask the owner to raise the limit.
483
+
484
+ ## 5.1. Server refusals are read from fields, not guessed (0.2.0)
485
+
486
+ Since 0.2.0 an API refusal carries structured fields instead of one free-form
487
+ sentence. It still arrives the way every tool result does — as JSON **inside the
488
+ text block**, with `isError: true`:
489
+
490
+ ```ts
491
+ { content: [{ type: "text", text: "{\"status\":\"FAILED\", …}" }], isError: true }
492
+ ```
493
+
494
+ So `JSON.parse(content[0].text)` first, then read the fields below. Reading
495
+ `content[0].balance_credits` gives `undefined`: the numbers are inside the parsed
496
+ text, not beside it. Do not relay the raw text to the user and do not guess from
497
+ it.
498
+
499
+ ```json
500
+ {
501
+ "status": "RETRY_LATER" | "FAILED",
502
+ "code": "rate_limited" | "insufficient_credits" | "debt_outstanding" | "http_503" | …,
503
+ "message": "<human-readable text from the server>",
504
+ "retryable": true | false,
505
+ "next_action": "<what to do right now>",
506
+ "retry_after_seconds": 7,
507
+ "balance_credits": 3,
508
+ "required_credits": 8,
509
+ "debt_credits": 120
510
+ }
511
+ ```
512
+
513
+ Numeric fields appear only where the refusal actually has them:
514
+
515
+ | Field | Appears on |
516
+ | --- | --- |
517
+ | `retry_after_seconds` | `rate_limited`, and any `http_5xx` where the server named a pause |
518
+ | `limit`, `window_seconds` | only when the 429 body followed the Clipwright contract; a 429 whose body did not carries neither |
519
+ | `balance_credits`, `required_credits` | `insufficient_credits` only |
520
+ | `debt_credits` | `debt_outstanding` only |
521
+
522
+ Treat every one of them as optional and read what is there. In particular, do not
523
+ expect balance numbers on `debt_outstanding`: that refusal reports the debt, and
524
+ the balance is not what blocks the run.
525
+
526
+ **One rule: look at `retryable`.**
527
+
528
+ - `retryable: true` (`rate_limited`, 5xx) — wait `retry_after_seconds` and repeat
529
+ **THE SAME call with the same arguments**. Do not bump `attempt`: this is not a
530
+ failed run but a refused request, and a new `attempt` would mean a second paid
531
+ render (section 5).
532
+ - `retryable: false` (`insufficient_credits`, `debt_outstanding`, other 4xx) —
533
+ **stop and tell the human**. A repeat gives the same answer while eating into
534
+ the rate limit. For money refusals, quote the NUMBERS: "the account has 3
535
+ credits, this needs 8" — without them the human cannot tell how much to buy.
536
+
537
+ The two money codes differ in their cure and must not be conflated:
538
+ `insufficient_credits` means "top up the balance", `debt_outstanding` means
539
+ "settle the debt", and topping up does NOT clear a debt. Advising "buy credits" on
540
+ the second code sends the human to spend money for nothing.
449
541
 
450
- Не путай это с `CLIPWRIGHT_CLIENT_ID`: `attempt` сознательное решение агента
451
- «это новая попытка», а `clientId` фиксированная идентичность установки, её
452
- менять между попытками НЕ нужно.
542
+ Do not read a source into the refusal. A `429` or a `5xx` can come from the
543
+ Clipwright API itself or from anything between you and it, and the fields above
544
+ cannot tell you which — so the report does not claim one, and neither should you
545
+ when relaying it. What is certain is the code and what to do about it.
453
546
 
454
- ### ⚠️ КАЖДЫЙ БАМП `attempt` СТОИТ ДЕНЕГ
547
+ About `429` specifically: it does **not** mean no run was created. The rate limit
548
+ sits in front of the handler, so a repeat carrying an already-used idempotency key
549
+ gets one too. Retry with THE SAME key — a new key is a new paid render.
455
550
 
456
- Новый `attempt` это новый ран и **новый платный вызов вендора**, а не
457
- бесплатная переотправка. Цикл «упало бампнул упало бампнул» тратит
458
- реальные кредиты на каждой итерации.
551
+ **Nobody waits on your behalf here you do the polling.** `get_run` makes one
552
+ call and hands you whatever came back, refusals included. So a `429` on a status
553
+ check is yours to sit out: wait `retry_after_seconds`, then call `get_run` again
554
+ with the same `run_id`. Do not treat it as a failed run and do not start a new
555
+ one — the run is already running and already paid for.
459
556
 
460
- Поэтому:
557
+ (The SDK's own `makeUgc` does wait these out internally, which is why the CLI and
558
+ direct SDK users never see them. The MCP tools are deliberately thin: `make_ugc`
559
+ returns as soon as the run starts, so a long render never blocks your turn.)
461
560
 
462
- - **перед повтором прочитай `error` упавшего рана.** Отказы вида
463
- `rejected_field`, `aspect_conflict`, `daily_cap_exceeded` повтором НЕ
464
- чинятся — они детерминированы входом или потолком, и следующая попытка
465
- упадёт так же, только уже за деньги;
466
- - **бампай `attempt` только при отказе, похожем на временный** (таймаут
467
- вендора, 5xx, обрыв сети);
468
- - **потолок существует и он суточный.** Исчерпав его, ты получишь
469
- `daily_cap_exceeded` — это сработавшая защита, а не поломка продукта.
470
- Дожидаться следующих суток или просить владельца поднять лимит.
561
+ ## 6. On failure plain curl as a diagnosis splitter
471
562
 
472
- ## 6. При отказе curl-скрипт как разделитель диагнозов
563
+ If the MCP path does not get you to a playable video, hit the same public REST
564
+ directly with curl, bypassing MCP, SDK and CLI entirely. If curl gets through, the
565
+ packaging layer is at fault; if it does not, production or the network is.
473
566
 
474
- Если MCP-путь не доводит до играющегося видео, запусти
475
- `scripts/clipwright-smoke.sh` (US-506) — он бьёт по тому же публичному REST
476
- `POST /v1/skills/make_ugc/run` → `GET /v1/runs/{id}` напрямую curl'ом, в обход
477
- MCP/SDK/CLI целиком:
567
+ **Two commands, no repository needed** (the package is installed from npm and
568
+ carries no scripts):
478
569
 
479
570
  ```bash
480
- CLIPWRIGHT_API_TOKEN=cw_... ./scripts/clipwright-smoke.sh "Привет! Тестовая фраза."
571
+ TOKEN=cw_... # the same value as CLIPWRIGHT_API_KEY, section 1.3
572
+ API=https://api-production-7e69.up.railway.app
573
+
574
+ # 1. Start a run. The idempotency key is required and must be UNIQUE per call:
575
+ # a repeated key returns the earlier run instead of a new one (section 2).
576
+ # Generate it with Node, not uuidgen: Node is certainly here (the package is
577
+ # installed via npx), while uuidgen may be missing on a bare Linux image — and
578
+ # then the shell silently substitutes an empty string, the key becomes
579
+ # constant, and a second run returns the earlier one, ruining exactly the
580
+ # diagnosis you started it for.
581
+ curl -sS -X POST "$API/v1/skills/make_ugc/run" \
582
+ -H "Authorization: Bearer $TOKEN" \
583
+ -H "Content-Type: application/json" \
584
+ -H "Idempotency-Key: cw-$(node -e 'process.stdout.write(crypto.randomUUID())')" \
585
+ -d '{"script":"Hello! This is a test line."}'
586
+
587
+ # 2. Poll until terminal, every ~5–10 s.
588
+ curl -sS "$API/v1/runs/<run_id>" -H "Authorization: Bearer $TOKEN"
481
589
  ```
482
590
 
483
- (`CLIPWRIGHT_API_TOKEN` то же значение, что `CLIPWRIGHT_API_KEY` в env MCP,
484
- см. раздел 1.3; `CLIPWRIGHT_API_URL` опционален, тот же дефолт-прод.)
485
-
486
- Диагноз по результату:
487
-
488
- - **curl доходит до `succeeded` со своим `run_id` и играющейся ссылкой, а MCP
489
- нет:** первопричина в слое MCP-пакетирования (сборка/конфиг хоста/env,
490
- раздел 1) не в REST API и не в доступе/токене. Это допустимое закрытие с
491
- документированным остаточным (курл-гейт Architect, US-511).
492
- - **curl тоже падает:** проблема не в MCP проверь `CLIPWRIGHT_API_TOKEN`
493
- (формат `cw_*`, не обрезан при копировании), сетевой доступ к
494
- `$CLIPWRIGHT_API_URL`, и (для истории про клонированные раны) что
495
- `CLIPWRIGHT_CLIENT_ID` реально уникален на этой машине (раздел 2) — если
496
- ответ 200 с ЧУЖИМ по смыслу `run_id`, это симптом дефекта 3.2 на MCP-пути,
497
- который curl него собственный, случайный `Idempotency-Key` на каждый
498
- вызов `scripts/clipwright-smoke.sh:112`) не воспроизведёт напрямую, но
499
- который стоит проверить отдельно через `get_run` на этот `run_id`.
500
-
501
- Скрипт также поддерживает бесплатный `--poll-only <run_id>` для проверки уже
502
- существующего рана без нового платного вызова.
503
-
504
- ## Итоговая последовательность (что должно воспроизвестись)
505
-
506
- 1. Установить сервер: `npx -y -p @clipwright/mcp-server@latest clipwright-mcp`
507
- стартует и печатает `running on stdio` (раздел 1.1). Из исходников —
508
- `pnpm install && pnpm build` из корня репо `dist/index.js` существует.
509
- 2. Сгенерировать уникальный `CLIPWRIGHT_CLIENT_ID` (раздел 2) и прописать его
510
- вместе с `CLIPWRIGHT_API_KEY` в конфиг MCP-хоста (раздел 1.3).
511
- 3. Перезапустить хост, убедиться, что `tools/list` отдаёт 4 тула
512
- (`make_ugc`, `quote_ugc`, `get_run`, `list_voices`).
513
- 4. `quote_ugc` со скриптом показать цену И прочитать `source`,
514
- `resolved_aspect_ratio`, `warnings[]` (раздел 3).
515
- 5. `make_ugc` получить `run_id` немедленно.
516
- 6. `get_run` каждые ~5–10 с, читая `state` в процессе и `status`+`state` на
517
- терминале, до `SUCCEEDED` (`video_url`) или `FAILED` (`error`).
518
- 7. При отказе — `scripts/clipwright-smoke.sh` (раздел 6) для разделения
519
- диагнозов.
520
-
521
- ### Если ты второй пользователь, проверяющий продукт
522
-
523
- Задача одна: **дойти до играющегося видео по этому файлу, без устной помощи
524
- владельца.** Проверяется не ты, а полнота инструкции — поэтому не спрашивай,
525
- а записывай, где застрял.
526
-
527
- Перед началом убедись, что в конфиге стоит **твой собственный**
528
- `CLIPWRIGHT_CLIENT_ID` (раздел 2). Без него сервер вернёт РАН ВЛАДЕЛЬЦА с
529
- ответом 200 и играющей ссылкой то есть ложный успех, который выглядит
530
- убедительнее настоящего.
531
-
532
- Отличай два класса отказа, они чинятся по-разному:
533
-
534
- - **`rejected_field`, `aspect_conflict`, `daily_cap_exceeded`** это
535
- сработавший контракт или сработавшая защита, а не дефект. Прочитай текст
536
- ошибки: в нём назван и повод, и замена;
537
- - **всё остальное** (таймаут, 5xx, пустой ответ, тул не найден) кандидат в
538
- настоящие дефекты. Здесь запусти `scripts/clipwright-smoke.sh`: если curl
539
- доходит до играющейся ссылки, а MCP нет — причина в слое упаковки MCP, и это
540
- надо записать отдельно от остального.
591
+ Reading the result:
592
+
593
+ - **curl reaches `succeeded` with its own `run_id` and a playable link, but MCP
594
+ does not:** the root cause is in the MCP packaging layer (build, host config,
595
+ environment — section 1), not in the REST API and not in access or the token.
596
+ - **curl fails too:** the problem is not MCP. Check `$TOKEN` from the commands
597
+ above (shaped `cw_*`, not truncated when copied), network access to `$API`, and —
598
+ for the cloned-run story that `CLIPWRIGHT_CLIENT_ID` really is unique on this
599
+ machine (section 2). If you get a 200 with a `run_id` that is not yours in
600
+ substance, that is the symptom of the collapse described in section 2 on the MCP
601
+ path; curl (which uses its own random `Idempotency-Key` per call) will not
602
+ reproduce it directly, but it is worth checking separately via `get_run` on that
603
+ `run_id`.
604
+
605
+ The second request above `GET /v1/runs/{id}` is free: you can poll an existing
606
+ run as often as you like without making a new paid call.
607
+
608
+ ## The whole sequence (what must reproduce)
609
+
610
+ 1. Install the server: `npx -y -p @clipwright/mcp-server@latest clipwright-mcp`
611
+ starts and prints `running on stdio` (section 1.1). From source:
612
+ `pnpm install && pnpm build` in the repository root, then `dist/index.js`
613
+ exists.
614
+ 2. Generate a unique `CLIPWRIGHT_CLIENT_ID` (section 2) and put it, together with
615
+ `CLIPWRIGHT_API_KEY`, into the MCP host config (section 1.3).
616
+ 3. Restart the host and confirm `tools/list` returns 4 tools (`make_ugc`,
617
+ `quote_ugc`, `get_run`, `list_voices`).
618
+ 4. `quote_ugc` with a script show the price AND read `source`,
619
+ `resolved_aspect_ratio`, `warnings[]` (section 3).
620
+ 5. `make_ugc` get a `run_id` immediately.
621
+ 6. `get_run` every ~5–10 s, reading `state` along the way and `status` + `state` on
622
+ the terminal response, until `SUCCEEDED` (`video_url`) or `FAILED` (`error`).
623
+ 7. On failure the same two curl commands from section 6 to split the diagnosis.
624
+
625
+ ### If you are a second user evaluating the product
626
+
627
+ You have one task: **reach a playable video using this file, without spoken help
628
+ from the owner.** What is being tested is the completeness of the guide, not you —
629
+ so do not ask, write down where you got stuck.
630
+
631
+ Before you start, make sure of two different things.
632
+
633
+ 1. **The token was issued on YOUR account, not merely as a separate token.** The
634
+ idempotency axis is `account_id`, and one account can hold several live tokens.
635
+ Your own account means another person's run cannot reach you at all. You cannot
636
+ check this from your side: the account behind a token is visible only to the
637
+ service. So the confirmation that the token was issued on a SEPARATE account
638
+ comes WITH the token — it is part of handing it over, not a question for the
639
+ owner. If it did not come, that is a gap in the instructions: write it down and
640
+ move on, no need to ask.
641
+ 2. **`CLIPWRIGHT_CLIENT_ID` was not copied from anyone's machine** (section 2). It
642
+ matters WITHIN an account: if you and the owner ended up on one account (even
643
+ under different tokens) AND your `clientId` matched, the server returns THE
644
+ OWNER'S RUN with a 200 and a playable link — a false success that looks more
645
+ convincing than a real one. It can match through a shared `HOME`, a copied
646
+ value, or a cloned image; a genuinely separate installation has its own even
647
+ without the environment variable.
648
+
649
+ Tell two classes of failure apart, because they are fixed differently:
650
+
651
+ - **`rejected_field`, `aspect_conflict`, `daily_cap_exceeded`** — a contract or a
652
+ protection doing its job, not a defect. Read the error text: it names both the
653
+ reason and the replacement;
654
+ - **everything else** (timeout, 5xx, empty response, tool not found) — a candidate
655
+ for a real defect. Here, run the same two curl commands from section 6: if curl
656
+ reaches a playable link and MCP does not, the cause is in the MCP packaging
657
+ layer, and that should be written down separately from the rest.