---
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/`