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/*, проксировать к реальному APIdo_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:
#!/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=")
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)
file:// не работает.
fetch(),canvas.toBlob()для cross-origin, чтение файлов по пути — всё заблокировано. Всегда через localhost.CORS блокирует даже localhost. Прокси нужен почти всегда.
.envне имеет MIME-типа. Файловый пикер скроет его. Убратьacceptатрибут с<input type="file">.Presigned URL содержит
?. Нельзя парсить простым split.Submit и status на разных URL. Не предполагать единый базовый путь.
GET + Content-Type = 405. Некоторые API отвергнут GET с Content-Type.
Пример: как выглядит диалог
Пользователь:
/playground Сделай интерфейс для [название скилла]
Агент (изучив скрипты): задаёт вопросы через AskUserQuestionTool — только те, что релевантны конкретному скиллу.
Пользователь: отвечает на вопросы.
Агент: Создаёт файлы, запускает, playground работает.