# Playground Builder — инструкция для агента

> Задача превратить skill или апи в автономный browser-based playground. Пользователь говорит Сделай плейграунд для [скилл], инструкция [ссылка] — агент сам разбирается в скриптах, задаёт вопросы и собирает интерактивный…

- Skill: `tools-only/playground-builder` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/playground-builder`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/playground-builder/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-29
- Page: https://skillmd.com/skills/tools-only/playground-builder

---

# Playground Builder — инструкция для агента

## Что это

Задача превратить skill или апи в автономный browser-based playground.
Пользователь говорит `Сделай плейграунд для [скилл], инструкция [ссылка]` —
агент сам разбирается в скриптах, задаёт вопросы и собирает интерактивный UI.

## Промпт пользователя

```
/playground Сделай playground для [название скилла]
```

Всё. Дальше работает агент.

---

## Инструкция для агента

### Шаг 1: Исследование скилла

Прочитай скрипты скилла.
Найди:
- Все API-эндпоинты (URL, метод, параметры, формат ответа)
- Все параметры командной строки (--prompt, --resolution и т.д.)
- Наличие API-ключей (config/.env)
- Формат входных данных (текст, файлы, URL)
- Формат выходных данных (изображение, текст, JSON, файл)
- Есть ли queue/polling (submit → poll status → get result)

### Шаг 2: Уточнение у пользователя

Используй AskUserQuestionTool чтобы задать вопросы.
Задавай только релевантные — не спрашивай про файлы если скилл текстовый.

Возможные вопросы:
- **Режим** — если скилл поддерживает несколько (generate/edit, analyze/export)
- **Язык UI** — русский или английский
- **Папка результатов** — куда сохранять
- **Итерации** — нужна ли возможность переиспользовать результат как вход
- **Дополнительные фичи** — рисование областей, шаблоны, пресеты

### Шаг 3: Сборка playground

Собери playground как набор файлов в указанной пользователем папке.

---

## Кейсы — что делать когда

### Кейс: В скрипте есть API-ключ

Если в скрипте есть загрузка из `.env` (паттерн `KEY=...`):
- Добавить в UI текстовое поле для вставки ключа
- Добавить file picker для загрузки .env файла
  - Убрать `accept` атрибут (`.env` не имеет MIME-типа, пикер его скроет)
  - В подсказке указать путь, в зависимости от того где находится скилл, например: `~/.claude/plugins/cache/[маркетплейс]/[скилл]/[версия]/skills/[скилл]/config`
  - Добавить hint: "Cmd+Shift+G для перехода по пути"
- Парсинг: `text.match(/KEY_NAME\s*=\s*(.+)/)`
- Хранить ключ только в `state` (память вкладки), не в localStorage

### Кейс: API не поддерживает CORS

Практически все API не отдают `Access-Control-Allow-Origin` для localhost.
Даже `file://` и `http://localhost` будут заблокированы.

Создать рядом с HTML:

**server.py** — прокси на stdlib Python:
- `http.server.SimpleHTTPRequestHandler` для статики
- `do_GET/do_POST/do_PUT` — если путь `/api/*`, проксировать к реальному API
- `do_OPTIONS` — вернуть 200 + CORS headers для preflight
- Заголовки: пробрасывать `Authorization`, `Content-Type` только для POST/PUT
- GET запросы: НЕ отправлять Content-Type (некоторые API ответят 405)
- `urllib.error.HTTPError` — проксировать код и тело, а не глотать
- Логирование: `print(f"[proxy] {method} {target}")` для отладки

**start.sh**:
```sh
#!/bin/sh
PORT=8090
echo "Playground: http://localhost:$PORT/[имя].html"
open "http://localhost:$PORT/[имя].html"
cd "$(dirname "$0")" && uv run server.py $PORT
```

### Кейс: Presigned URL upload (двухшаговая загрузка)

Паттерн: POST initiate → получить `{upload_url, file_url}` → PUT файл на `upload_url`.

Presigned URL содержит query-параметры (`?signature=...&token=...`).

Проксирование:
- В HTML: `fetch('/api/put?url=' + encodeURIComponent(presignedUrl), {method: 'PUT', body: file})`
- В server.py: парсить через `urllib.parse.parse_qs`, НЕ через `split("?url=")`

```python
elif path.startswith('/api/put?'):
    qs = urllib.parse.urlparse(path).query
    target = urllib.parse.parse_qs(qs).get('url', [''])[0]
```

### Кейс: Queue-based API (длинный polling)

Паттерн: submit → получить request_id → poll status → get result.

**ВАЖНО**: URL для submit и для status/result могут отличаться.
Всегда проверять все URL в скрипте — не предполагать единый базовый путь.

В HTML:
- Прогресс-бар с анимацией
- Polling с разумным интервалом (2-5 сек)
- Таймаут
- Обработка всех статусов из скрипта (success, fail, in progress, queued и т.д.)
- При завершении — забрать результат (эндпоинт может отличаться от status)

В server.py: отдельные маршруты для submit и status, если URL различаются.

### Кейс: Скилл работает с файлами (изображения, документы)

- Drag & drop зона с превью миниатюр
- `<input type="file">` как fallback
- Для изображений: отображение на `<canvas>`
- Для canvas: возможность рисовать прямоугольные области (mousedown → mousemove → mouseup)
- Каждая область — текстовая заметка в sidebar
- Заметки включаются в промпт с координатами в процентах от размера изображения

Загрузка default-изображения (если файл рядом с HTML):
- Использовать `canvas.toBlob()` для конвертации в File, НЕ `fetch()`
- Обернуть в `try/catch` — из `file://` canvas будет tainted

### Кейс: Результат можно итерировать

Если результат того же типа что и вход (изображение → изображение):
- Сохранять URL/blob каждого результата в массив `state.history`
- Показывать миниатюры в sidebar
- Клик по миниатюре → загрузить как новый исходник
- Кнопка "Вернуть оригинал"
- После генерации: обновить `state.imageFile` из результата для следующей итерации

### Кейс: AI-обратная связь через Claude CLI

Если результат выигрывает от анализа или итерации с помощью AI:
- Добавить в server.py эндпоинт `/api/claude`
- Сервер вызывает `claude -p "prompt" --output-format json` через `subprocess.run()`
- Для сохранения контекста: `claude --resume SESSION_ID -p "prompt"`
- В HTML: кнопка "Спросить Claude" отправляет результат + вопрос на `/api/claude`

### Кейс: Язык интерфейса

Всегда спрашивать через AskUserQuestion. Не угадывать по языку промпта.

Если пользователь выбрал русский:
- Все элементы UI: заголовки, label, placeholder, hint, кнопки, статусы, ошибки
- Кнопки: "Запустить", "Скопировать", "Очистить все", "Скачать", "Вернуть оригинал"
- Статусы: "Загружаю...", "Генерация... (обычно 15-30 сек)", "Готово!", "Ошибка: ..."

---

## Архитектура playground

### Файлы

```
output-folder/
  [имя]-playground.html   — UI, единый файл, inline CSS + JS
  server.py               — прокси (если нужен)
  start.sh                — запуск (если нужен прокси)
```

### UI-требования

- Единый HTML-файл, inline CSS + JS
- Тёмная тема
- Кнопка запуска: `disabled` пока не заполнены обязательные поля
- Прогресс/статус: пользователь должен видеть что происходит
- Лейаут подбирать под задачу скилла (sidebar + preview, полноэкранный и т.д.)

---

## Ловушки (must read)

1. **file:// не работает**. `fetch()`, `canvas.toBlob()` для cross-origin,
   чтение файлов по пути — всё заблокировано. Всегда через localhost.

2. **CORS блокирует даже localhost**. Прокси нужен почти всегда.

3. **`.env` не имеет MIME-типа**. Файловый пикер скроет его.
   Убрать `accept` атрибут с `<input type="file">`.

4. **Presigned URL содержит `?`**. Нельзя парсить простым split.

5. **Submit и status на разных URL**. Не предполагать единый базовый путь.

6. **GET + Content-Type = 405**. Некоторые API отвергнут GET с Content-Type.

---

## Пример: как выглядит диалог

**Пользователь:**
```
/playground Сделай интерфейс для [название скилла]
```

**Агент** (изучив скрипты): задаёт вопросы через AskUserQuestionTool —
только те, что релевантны конкретному скиллу.

**Пользователь:** отвечает на вопросы.

**Агент:** Создаёт файлы, запускает, playground работает.

