---
tags:
  - type/plan
  - topic/vibe-coding
date: 2026-06-05
---

# mars-bot — MVP дизайн

Бот для школы Mars. MVP — отправка саммари митингов в команду через slash-команду в Claude. Заложен под рост: позже добавится фича уведомлений об упоминаниях Mars-каналов в Telegram (отдельный долгоиграющий процесс на Pi).

## Контекст

Уже есть рабочий конвейер:
- `granola-sync` качает митинги из Granola в `_inbox/`
- `/sort-meetings` разносит их по `projects/<project>/meetings/`
- В `projects/mars/meetings/` копится 30–40 транскриптов в месяц

Существующая практика: для отдельных митингов вручную создаются саммари в виде файлов `{mars} {article} саммари ...`. Шеринг в команду — пока вне процесса.

Цель MVP: упростить шаг «нужный митинг → выжимка → в чат команды» одной разговорной командой в Claude.

## Workflow

1. Митинг прошёл, транскрипт лежит в `projects/mars/meetings/{mars} {transcript} ... .md`.
2. Наташа в Claude вызывает `/send-summary` (с указанием файла и чата или без — Claude дозапросит).
3. Claude читает транскрипт, генерит саммари по эталонной структуре (главные боли, решения и идеи, прочее), показывает превью в чате.
4. Наташа смотрит превью. При желании просит поправить — итерирует с Claude в чате до нужного состояния. Это естественный review-gate.
5. Когда довольна — говорит «отправляй». Claude записывает готовый текст саммари во временный файл (`/tmp/mars-bot-summary-<timestamp>.md`), вызывает `mars-bot send --to <alias> --text-file <path>`, и **в finally удаляет временный файл — независимо от того, успешной была отправка или нет**. Если CLI/отправка упала — Claude сообщает об ошибке в чате; повторная отправка требует от Наташи нового подтверждения (текст саммари ещё в контексте чата с Claude, новый temp-файл создаётся заново).
6. CLI шлёт в Telegram через Bot API. Текст саммари остаётся жить только в TG-чате (в vault не сохраняется).

Batch-режим — естественно через диалог: «вот три митинга — саммари каждому и отправь в team», «вот этот в team и в marketing». Claude генерит все, показывает превью скопом, ты ревьюишь, потом Claude вызывает CLI нужное число раз. Никакой специальной batch-логики в CLI — она вся в slash-команде и в Claude.

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

CLI-утилита на Mac. Запускается локально при вызове slash-команды. Никакого долгоиграющего процесса — Telegram Bot API позволяет любому процессу с токеном слать сообщения от имени бота, listener'а для отправки не нужно.

Один TG-бот (одна учётка у BotFather). Сейчас единственный потребитель токена — CLI. Когда позже понадобится фича упоминаний — рядом появится `bot.py` для запуска как daemon на Pi (как `tasks-bot`, `conference-bot`), который будет делить с CLI токен и конфиг чатов.

### Структура репо

```
projects/vibe-coding/mars-bot/
  src/mars_bot/
    __init__.py
    config.py          # загрузка .env + chats.json
    telegram.py        # обёртка над Bot API (send_message)
    format.py          # md → HTML конвертация + дробление длинных
    cli.py             # точка входа: mars-bot send --to <alias>
  scripts/
    get_chat_ids.py    # утилита setup'а — печатает chat_id из getUpdates
  data/
    sent.log           # лог всех отправок (не в git)
  .env.example
  chats.json.example
  pyproject.toml
  README.md
  docs/
    {vibe-coding} {plan} mars-bot mvp дизайн – 2026-06-05.md  (этот файл)
  tests/
    test_format.py
    test_cli.py
```

`.env`, `chats.json`, `data/` — в `.gitignore`.

## CLI

Команда:

```
mars-bot send --to <alias> [--text-file PATH | --text STRING | через stdin]
mars-bot send --to <alias> --dry-run
```

Поведение:
- Один вызов = одно сообщение в один чат
- Дробление — двухпроходное, потому что лимит TG (4096 chars) применяется к итоговому HTML, а эскейп (`&` → `&amp;`, `<` → `&lt;`, `>` → `&gt;`) и обёртки тегов (`<b>...</b>`) удлиняют текст:
  - **Проход 1 (markdown).** Режем исходный markdown по границам абзацев (`\n\n`) с запасом, целясь в **3500 символов markdown** (а не 4000) — чтобы оставить место под прирост от эскейпа и тегов. Каждый кусок конвертится в HTML отдельно. Это сохраняет инвариант: HTML-теги (`<b>`, `<i>`, `<a>`) не пересекают границу абзаца и не разорвутся.
  - **Проход 2 (HTML, страховка).** После конвертации проверяем длину каждого HTML-чанка. Если какой-то всё же > 4000 (например, был абзац под завязку с кучей `&` в ссылках) — дополнительно режем этот чанк по предложениям. Если предложение всё ещё > 4000 — режем по словам как последняя страховка от падения.
- Кейс «один абзац > 3500» — режется по предложениям ещё на первом проходе.
- `--dry-run` печатает в stdout что бы улетело, не шлёт
- После успешной отправки — запись в `data/sent.log`
- Падает с понятным сообщением при: невалидном алиасе чата (не в `chats.json`), отсутствии токена, ошибке Bot API, превышении rate-limit

Whitelist алиасов в CLI: попытка слать в `chat_id`, которого нет в `chats.json`, — отказ ещё до вызова Bot API. Это защита от опечаток и багов в slash-команде, **не** замена секретности токена: тот, у кого есть токен, может напрямую дёрнуть Bot API и отправить в любой чат, где бот состоит и имеет право писать. Whitelist срабатывает только для отправок через CLI.

## Slash-команда `/send-summary`

Файл: `.claude/commands/send-summary.md`. Инструкция для Claude, как обрабатывать запросы саммаризации и отправки.

Содержит:
- Описание workflow (что делать пошагово)
- Гайд по структуре саммари (главные боли, решения и идеи, прочее — с эталонным примером, аналогичным `{mars} {article} саммари ретро по доду – 2026-06-03.md`)
- Логика обработки аргументов:
  - Если файл не указан — показать последние 5 транскриптов из `projects/mars/meetings/` (по дате в имени), спросить какой
  - Если чат не указан — показать алиасы из `chats.json`, спросить какой
- Правило: всегда показать превью саммари до отправки, дождаться явного подтверждения от Наташи
- Batch: если указано несколько файлов или чатов — обработать каждую пару, показать все превью скопом, потом отправлять
- Команда CLI: Claude всегда записывает текст саммари во временный файл `/tmp/mars-bot-summary-<timestamp>.md` и вызывает `mars-bot send --to <alias> --text-file <path>`. Heredoc через bash намеренно **не используется** — текст саммари может содержать `$`, backticks, кавычки, случайные совпадения с delimiter'ом; временный файл устраняет проблему экранирования
- Lifecycle временного файла: **всегда** удаляется после вызова CLI, в finally, независимо от исхода (успех или ошибка). Это правило в slash-команде. При ошибке отправки Claude сообщает об ошибке в чате; повторная попытка требует нового подтверждения от Наташи и создания нового temp-файла. Так в `/tmp/` не копится мусор и нет риска оставить чувствительный текст саммари в файле дольше нужного.

Тон/стиль саммари — деловой, конспективный. Это рабочий документ для команды, не контент в канал. Skill `writing-natasha` тут не применяется.

## Конфигурация

`.env`:
```
TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
```

`chats.json`:
```json
{
  "team": -100123456789,
  "marketing": -100987654321,
  "ops": -100111222333,
  "test": 12345678
}
```

Алиас `test` — личка Наташе. Используется для smoke-тестов и предотправочной проверки формата без шума в командных чатах.

Реальные имена алиасов и `chat_id` заполняются Наташей на этапе setup'а — в спеке только структура.

## Setup (одноразовый)

1. Создать бота через @BotFather: `/newbot` → имя → username → получить токен.
2. Положить токен в `.env` (скопировать `.env.example`).
3. Добавить бота как участника в каждый Mars-чат (нужны права админа у Наташи в чате).
4. В каждом чате после добавления бота отправить хотя бы одно сообщение — иначе чат не появится в `getUpdates`.
5. Запустить `python scripts/get_chat_ids.py` — скрипт дёрнет `getUpdates` у Bot API и напечатает список:
   ```
   Команда Mars (id=-100...)
   Маркетинг Mars (id=-100...)
   ...
   ```
6. Скопировать `chat_id` в `chats.json` под нужными алиасами (`.env.example` → копируем в `.env`, `chats.json.example` → в `chats.json`).
7. Smoke-test: `mars-bot send --to test --text "хей"` — должно прийти Наташе в личку.

### Setup troubleshooting

- **`getUpdates` возвращает 409 Conflict или пустой массив.** У бота может быть установлен webhook (даже остаточный от прежних экспериментов). Снять: `curl https://api.telegram.org/bot<TOKEN>/deleteWebhook`. После — повторить п. 5.
- **`test`-алиас (личка) не появляется в `getUpdates`.** Открыть бота в TG, нажать `/start` или просто написать любое сообщение. Без явного контакта в личке Bot API чат не отдаёт.
- **Групповой чат не появляется в `getUpdates`.** Проверить два момента: (1) после добавления бота в группе кто-то должен либо упомянуть бота (`@mars_bot привет`), либо отправить команду (`/start@mars_bot`) — иначе из-за privacy mode бот не получит апдейт; (2) если упоминание не помогает — у @BotFather: `/mybots` → бот → `Bot Settings` → `Group Privacy` → `Turn off`. После этого повторить упоминание/команду в группе и снова `getUpdates`.
- **Bot API возвращает 403 Forbidden при отправке.** Бот был удалён из чата или ему заблокировали возможность писать. Добавить заново.

## Формат отправки в TG

**parse_mode=HTML.** Конвертация исходного markdown:
- `**bold**` → `<b>bold</b>`
- `*italic*` → `<i>italic</i>`
- `# H1`, `## H2`, `### H3` → `<b>...</b>` + перенос
- `[text](url)` → `<a href="url">text</a>`
- Списки `- item` и `* item` в начале строки конвертируются в `• item` (буллеты читабельнее в TG, чем строчный дефис, который легко спутать с минусом)
- HTML-спецсимволы в теле (`<`, `>`, `&`) — эскейпим стандартно (`&lt;`, `&gt;`, `&amp;`)

Итоговое сообщение:

```
📝 <b>Саммари: {заголовок} — {дата}</b>

{тело саммари с HTML-форматированием}
```

Заголовок Claude формирует при генерации (из имени файла или из первого `# ...` в саммари).

Дробление — рекурсивное, но **режется только markdown**, HTML-строка никогда не разрезается:
- Проход 1: режем исходный markdown по абзацам с целью **3500 char** (запас под прирост от HTML-эскейпа и тегов), каждый кусок конвертится в HTML отдельно.
- Если после конвертации какой-то HTML-чанк всё ещё > 4000 char (плотный текст с обилием `&` или длинных ссылок) — берём **исходный markdown** этого куска и рекурсивно мельчим его с уменьшенным target (target/2), конвертируем заново. Цикл до пола `MIN_MD_TARGET = 500`.
- На полу — fallback по предложениям и словам, всё ещё на уровне markdown, потом конвертация каждого фрагмента.

Инвариант: ни одна операция `split` не применяется к строке HTML. Это исключает поломку `&amp;`, `<a href="...">`, `<b>...</b>`, которая случилась бы при наивной нарезке HTML посередине.

## Безопасность и kill-switch

**Главный аварийный стоп: revoke токена через @BotFather.** Мгновенно делает токен мёртвым. Шаги (в README отдельным блоком «Emergency stop»):
1. @BotFather → `/mybots` → выбрать `mars-bot` → `API Token` → `Revoke current token`
2. Старый токен мёртв. Никакой код больше не сможет слать от имени бота.
3. Когда разобрались — там же сгенерить новый токен, обновить `.env`.

Дополнительные слои защиты в коде:

1. **Whitelist алиасов в CLI.** Защита от ошибок и багов при отправках через CLI: алиас, отсутствующий в `chats.json`, отвергается без вызова API. **Не** защищает от утечки токена — секретность токена обеспечивается отдельно (см. п. 4).
2. **Лог отправок** в `data/sent.log`: `timestamp | chat_alias | first 200 chars of text`. Постфактум видно что улетело и куда.
3. **Rate-limit через файл состояния.** Не больше 20 **Telegram-сообщений** в час (а не вызовов CLI: длинное саммари, разрезанное на N чанков, считается как N сообщений). Состояние хранится в `data/sent.log`: одна строка на каждый успешно отправленный чанк. Перед отправкой CLI читает лог, считает записи за последние 60 минут и сверяет с числом чанков для текущего вызова — если `recent + chunks_to_send > 20`, отказывает целиком (без частичной отправки, чтобы не урезать саммари). Файловое состояние нужно потому, что каждый вызов CLI — новый процесс. При частичном падении (chunk N упал после успешных N-1) — успешные чанки логируются по факту, на stderr сообщается «sent N/M before failure».
4. **Хранение токена:** `.env` только локально на Mac, в `.gitignore`. Реалистично утечь может только при физическом/удалённом доступе к компьютеру.

## Тестирование

- `tests/test_format.py`:
  - md → HTML конвертация (bold, italic, заголовки, ссылки, списки)
  - Эскейп HTML-спецсимволов в теле (`<`, `>`, `&`) — должны стать `&lt;`, `&gt;`, `&amp;`
  - Эскейп `&` внутри ссылок (`?a=1&b=2`)
  - Звёздочки внутри слов (`a*b*c` не должен случайно стать italic)
  - Незакрытый `**` — должен либо корректно проигнорироваться, либо упасть с понятной ошибкой (не отдавать сломанный HTML)
  - Дробление длинных: текст с тремя абзацами по 2000 char каждый → три отдельных сообщения; границы абзаца не разрываются
  - Граничный случай: один абзац > 4000 → дробится по предложениям
- `tests/test_cli.py` — парсинг аргументов, whitelist алиасов, чтение `--text-file` и stdin, dry-run, rate-limit (21-я отправка отвергается)
- `telegram.py` мокается в тестах (без реальных HTTP-вызовов к Bot API)
- Smoke-test после setup'а: отправить тестовое сообщение в `test`-алиас

## Out of scope (MVP)

- Автоматическая саммаризация по новым транскриптам (без явной команды Наташи) — потенциально позже
- Хранение саммари в файлах vault — намеренно нет, текст живёт только в TG
- Долгоиграющий процесс на Pi — появится с фичей упоминаний
- Reply-кнопки или интерактив в TG-чатах — бот шлёт one-way, на ответы не реагирует
- Авторизация в CLI (кроме самого `.env` на Mac) — CLI запускается только локально Наташей
- Веб-интерфейс, статус-страница, мониторинг — для CLI нет смысла
- Уведомления об упоминаниях каналов — следующий проект, отдельный документ

## Критерии готовности

- Создан репо `projects/vibe-coding/mars-bot/` с описанной структурой
- `.env.example`, `chats.json.example`, `pyproject.toml`, `README.md` на месте
- CLI `mars-bot send` работает: парсит аргументы, читает stdin, конвертит markdown в HTML, дробит длинные, шлёт через Bot API, логирует в `sent.log`
- Whitelist чатов работает: попытка слать в неизвестный алиас — внятная ошибка без вызова API
- Rate-limit работает: 21-е сообщение в часе — отказ
- `scripts/get_chat_ids.py` печатает список доступных чатов
- Тесты проходят
- Slash-команда `.claude/commands/send-summary.md` создана с гайдом структуры саммари и описанием workflow
- README содержит секцию «Emergency stop» с шагами revoke
- Smoke-test пройден: сообщение из CLI ушло в `test`-чат

## Заготовка под будущее (упоминания каналов)

Не делается в MVP, но архитектура учитывает:

- Тот же репо `mars-bot/`, та же `.env`, тот же `chats.json`
- Рядом с `cli.py` появится `bot.py` — долгоиграющий процесс на Pi
- Будет периодически опрашивать (TGStat API или другой источник) появления упоминаний `@marsingru`, `@choooooooir`, `@tvorcheskiye_lyudi`
- Найденные упоминания слать Наташе в личку (`test`-алиас или отдельный)
- Деплой на Pi через systemd (по образцу `tasks-bot`, `conference-bot`)
- Возможно появится новый алиас в `chats.json` (например `natasha_alerts`) для этих уведомлений

Это отдельный проект, отдельный спека-документ — текущий MVP его не закрывает и не блокирует.
