--- pr: 174 title: feat(telegram): add telegram-send plugin-tool for Bot API messaging --- ## Что сделано - `.opencode/telegram/` — CLI-проект (по аналогии с `.opencode/draw-image/`): - `package.json` — `"type": "module"`, без runtime-deps; devDeps: `@types/node`, `typescript`, `vitest`; script `test: vitest run` - `tsconfig.json` — `target: ES2022`, `module: ESNext`, `moduleResolution: Bundler`, `strict`, `allowImportingTsExtensions: true`, `lib: ["ES2022", "DOM"]` (DOM нужен для типов `fetch`, `BodyInit`, `Blob`, `FormData`, `File` — все используются в `src/api.ts`) - `vitest.config.ts` — `environment: "node"`, `include: ["tests/**/*.test.ts"]` (e2e.manual.ts исключён — нет `.test.` в имени) - `src/config.ts` — `loadConfig(overrides?)`: читает `TELEGRAM_BOT_TOKEN`/`TELEGRAM_CHAT_ID` из env, валидирует, override chatId имеет приоритет над env - `src/markdown.ts` — `escapeMarkdownV2(text)`: экранирует спецсимволы `_*[]()~\`>#+-=|{}.!` (backslash НЕ в наборе), кириллицу не трогает - `src/api.ts` — `sendMessage`/`sendDocument`/`sendPhoto` через встроенный `fetch` Node 22+; `telegramFetch` парсит `result`, возвращает `{ ok, message_id, chat_id }`; при `ok:false` — throw с `description` - `cli.ts` — argv-парсер, диспетчер по `action` (text/document/photo), `parse_mode` по умолчанию `"MarkdownV2"`; успех → `console.log(JSON.stringify(result))`, провал → stderr + `exit(1)` - `.opencode/tools/telegram-send.ts` — plugin-tool обёртка (~35 строк): `spawnSync("node", ["--experimental-strip-types", cliPath, ...cliArgs])`, при `status !== 0` возвращает `⚠️ telegram-send failed (exit N): ...`, иначе `stdout.trim()` - Тесты в `.opencode/telegram/tests/` (39 unit-тестов, все проходят): - `markdown.test.ts` — 25 кейсов: пустая строка, без спецсимволов, кириллица, каждый спецсимвол `it.each`, микс кириллицы+спецсимволов, все спецсимволы сразу, backtick, `\` не в наборе - `config.test.ts` — 5 кейсов: валидный env, отсутствие token, отсутствие chatId, override приоритет, override без env - `api.test.ts` — 9 кейсов: URL/method/body/headers sendMessage, parse_mode omit/plain/HTML, возвращаемое значение, Telegram API error, network error, sendDocument FormData, sendPhoto FormData (spy на `FormData.prototype.append`) - `e2e.manual.ts` — ручной скрипт (НЕ в `npm test`) - `.env.example` — добавлена `TELEGRAM_CHAT_ID=your-telegram-chat-id` в секцию `# Telegram (optional)` (после `TELEGRAM_BOT_TOKEN`) ## Почему Оркестратору нужен быстрый способ показывать пользователю результаты работы — например, сгенерированный `draw-image` PNG или .md-файл с архитектурой проекта. Сейчас единственный способ показать файл — через tunnel skill (запуск процесса, проброс порта, ссылка), что тяжело для разовой отправки. Прямая отправка в Telegram через Bot API решает это одним вызовом tool'а: `telegram-send({ action: "photo", path })` — и файл у пользователя. Plugin-tool (не MCP-сервер) выбран как паттерн: авто-дискаверится opencode из `.opencode/tools/`, не требует записи в `opencode.json`, тонкая TS-обёртка дёргает CLI через `spawnSync`. Без runtime-зависимостей — только встроенный `fetch`/`FormData`/`Blob` Node 22+. ## Pending - Реальная E2E-проверка с живым ботом (нужны `TELEGRAM_BOT_TOKEN` + `TELEGRAM_CHAT_ID` в env) — `node --experimental-strip-types tests/e2e.manual.ts` - Проверка цепочки `draw-image` → `telegram-send photo` в реальном сценарии оркестратора - Возможно: интеграция `escapeMarkdownV2` в оркестратор для авто-экранирования текста перед `action: "text"` ## Watch out - `parse_mode: "MarkdownV2"` — формат по умолчанию в `cli.ts:29`, но НЕ в `api.ts` (api-функции добавляют `parse_mode` в body только когда явно передан `MarkdownV2`/`HTML`; `undefined`/`"plain"` → поле отсутствует). Это сознательно: api.ts — низкоуровневый, cli.ts — применяет дефолт - `lib: ["ES2022", "DOM"]` в `tsconfig.json` — DOM нужен только для типов `fetch`/`BodyInit`/`Blob`/`FormData`/`File`; рантайм-значения берутся из Node 22+ глобалов (НЕ из браузера) - `allowImportingTsExtensions: true` — сознательный паттерн проекта (как `draw-image`), т.к. запуск через `node --experimental-strip-types` с `.ts`-импортами - Node 22+ требуется (встроенные `fetch`, `Blob`, `FormData`, `File` глобально) - `tests/e2e.manual.ts` исключён из `npm test` двумя механизмами: отсутствие `.test.` в имени + `include: ["tests/**/*.test.ts"]` в vitest config — двойная защита - Plugin-tool НЕ MCP-сервер — НЕ добавлять в `opencode.json`, авто-дискаверится из `.opencode/tools/`