Skip to main content
Glama

herald

Канал наружу. Ассистент отправляет готовый материал — текст, таблицу, файл, скриншот — в твой рабочий мессенджер, откуда ты пересылаешь его дальше.

Статус: рабочая версия: отправка сообщений через MCP и захват рабочих чатов в локальный буфер.


Зачем

Две причины, и вторая важнее.

Очевидная. Таблицы, длинные разборы и скриншоты копируются из терминала криво. Форматирование рассыпается, картинку вообще не скопируешь. Ты говоришь «отправь это в такой-то топик» — и материал приходит целым.

Неочевидная: это чинит авторство.

Сейчас, когда ты вручную пересылаешь текст ассистента в рабочий чат, он уходит под твоим именем. В выгрузке эти сообщения неотличимы от твоих собственных — ни пометки, ни поля. Проверено на живом чате: тринадцать сообщений от одного автора, часть из которых написана машиной, и различить их можно только по стилю. Догадка по стилю — это ровно то, что запрещает mnemo: не угадывать.

Если пишет бот, а ты пересылаешь его сообщение — Telegram сохраняет forwarded_from с именем бота. Машинный текст становится машинно опознаваемым. Автоматически, без меток и дисциплины.

Устройство

Ядро — MCP-сервер с токеном и транспортом. Небольшой общий скилл поверх него учит Claude/Codex выбирать структурированную команду и соблюдать стиль.

ассистент ──MCP──> herald ──адаптер──> Telegram (топик группы)
                              │
                              └──> другие платформы, когда появятся

Интерфейс узкий: «отправить <это> в <туда>». Внутри — один адаптер под Telegram.

Абстракцию под платформы заранее не строим. Интерфейс появляется, когда платформ становится две, а не в ожидании второй. Так вышло с парсерами в mnemo: реестр завели на втором формате, и он сразу был правильной формы, потому что опирался на два реальных случая, а не на догадку об одном.

Безопасность — свойством, а не процедурой

herald пишет только в твой стейджинг. Начальству пересылаешь ты, руками.

Это сильнее, чем «бот с подтверждением»: он физически не может отправить что-то не то в чат с заказчиком, потому что не знает туда дороги. Гарантия структурная, а не «мы договорились подтверждать».

Правило: список разрешённых адресатов задаётся в конфиге и не расширяется ассистентом.

Что нужно для запуска

  • токен бота (BotFather), хранится вне репозитория;

  • идентификатор группы-стейджинга и карта топиков «имя → id»;

  • бот добавлен в группу с правом писать.

Первая версия

Сейчас herald — локальный Python MCP-сервер со stdio-транспортом. Он предоставляет десять команд:

  • list_destinations — посмотреть разрешённые проекты и их маршруты;

  • send_client_copy — готовый для пересылки клиенту текст с реальными темами вместо служебных разделов;

  • send_update — внутренний управленческий апдейт из отдельных полей;

  • send_file — отправка файла или изображения из разрешённого каталога;

  • send_text — ручная отправка точного или нестандартного текста;

  • notify_completion — уведомление о завершении задачи, когда у задачи явно установлен такой флаг или дана такая инструкция.

  • inbox_status, inbox_fetch, inbox_export, inbox_done — состояние, чтение, выдача и подтверждение локального буфера захваченных сообщений.

Команды проходят один путь: проект → маршрут → адаптер → Telegram. Сам herald не наблюдает за задачами и не решает, когда уведомлять: это делает Codex, Claude или их lifecycle-hook.

Каждое сообщение получает компактную подпись:

— Codex · GPT · herald · MCP

Установка плагином

Нужен установленный uv. Плагин приносит один и тот же MCP-сервер и один и тот же skill в Claude Code и Codex: отдельные mcp add, симлинки и копии навыка не нужны.

Claude Code:

claude plugin marketplace add ZenonEl/herald
claude plugin install herald@herald --scope user

Codex:

codex plugin marketplace add ZenonEl/herald
codex plugin add herald@herald

После установки открой новую сессию. Установленный skill называется herald:herald-send: в Claude Code его можно вызвать как /herald:herald-send, в Codex — как $herald:herald-send. Обычная просьба «отправь через Herald» также должна активировать его по описанию.

Плагин не содержит токен и рабочие адресаты. Создай пользовательский конфиг:

mkdir -p ~/.config/herald
curl -fsSL https://raw.githubusercontent.com/ZenonEl/herald/main/config.example.toml \
  -o ~/.config/herald/config.toml
printf '%s\n' 'TOKEN_FROM_BOTFATHER' > ~/.config/herald/telegram.token
chmod 600 ~/.config/herald/telegram.token

Открой ~/.config/herald/config.toml и замени пример своими разрешёнными группами, топиками и проектами. Проверить подключение можно просьбой «покажи направления Herald» в новой Claude/Codex-сессии.

Обновление плагина

Claude Code:

claude plugin marketplace update herald
claude plugin update herald@herald --scope user

Codex:

codex plugin marketplace upgrade herald
codex plugin add herald@herald

После обновления тоже нужна новая сессия: уже открытая продолжает работать со старым набором skill/MCP-инструментов.

Если Herald раньше подключался вручную, перед установкой плагина убери старую MCP-запись командами codex mcp remove herald и claude mcp remove herald. Проверь старые пути ~/.agents/skills/herald-send и ~/.claude/skills/herald-send: если это именно символические ссылки на checkout, удали ссылки через unlink, не затрагивая сам репозиторий. Пользовательский ~/.config/herald/ при миграции сохраняется.

Конфиг и версии

Проект использует uv. Версия Python зафиксирована в .python-version, версия пакета и зависимости — в pyproject.toml, точные версии — в uv.lock.

Версии следуют Semantic Versioning: pyproject.toml — SSOT версии пакета, менять её нужно через uv version --bump patch|minor|major. Каждый публичный выпуск получает подписанный тег vX.Y.Z, GitHub Release и запись в CHANGELOG.md; версия тега обязана совпадать с project.version. Тесты дополнительно сверяют версию пакета с обоими плагин-манифестами и marketplace Claude Code.

В ~/.config/herald/config.toml задаются платформы, разрешённые маршруты, проекты и каталоги файлов. Это SSOT: MCP-команда может выбрать только существующий маршрут, а файл — только путь внутри allowed_roots. Конфиг перечитывается перед каждым вызовом, поэтому после добавления проекта или топика сервер перезапускать не нужно. Telegram-токен по умолчанию читается из отдельного файла:

printf '%s\n' 'TOKEN_FROM_BOTFATHER' > ~/.config/herald/telegram.token
chmod 600 ~/.config/herald/telegram.token

Сам токен и рабочий конфиг не хранятся в репозитории. Вместо файла можно задать token_env = "HERALD_TELEGRAM_BOT_TOKEN" в секции платформы.

Чтобы узнать chat_id и topic_id, добавь бота в staging-группу, отправь сообщение в нужный топик и посмотри ответ getUpdates: message.chat.id — это chat_id, а message.message_thread_idtopic_id.

Проект задаёт маршрут по умолчанию:

[routes.demo-shop]
platform = "telegram"
chat_id = "-1001234567890"
topic_id = 2

[projects.demo-shop]
label = "Demo Shop"
description = "Демонстрационный интернет-магазин и относящиеся к нему материалы."
route = "demo-shop"

[files]
allowed_roots = ["~/Herald/outbox"]
max_bytes = 50000000

Создай каталог ~/Herald/outbox и клади туда только то, что разрешено отправлять ассистенту. Не открывай ему целиком домашний каталог или все рабочие репозитории: среди них часто лежат .env, ключи и клиентские данные.

Обычно ассистент передаёт только project = "demo-shop"; поле route нужно лишь для явного переопределения. subject — краткая тема для подписи сообщения, а не Telegram topic_id.

Форматирование

Формат нужно выбрать явно: plain либо html. Для обычного сообщения человеку предпочтителен format = "html"; herald добавит parse_mode = "HTML". Поддерживаются теги Telegram вроде <b>, <i>, <u>, <s>, <tg-spoiler>, <a href="…">, <code>, <pre> и <blockquote>. Служебная подпись и ссылка экранируются самим herald, HTML основного текста передаётся как есть.

Если при format = "html" передать экранированные теги вроде &lt;b&gt;, herald отклонит вызов и подскажет передать сырой <b>. Это не даёт ассистенту молча прислать видимые HTML-теги вместо форматирования.

Пресеты сообщения

Для send_update сервер сам собирает HTML из полей summary, completed, blockers, decisions_needed, client_questions и next_steps, экранирует значения и проверяет длину каждого пункта. Это основной путь для статусов и отчётов: модель не присылает готовую портянку.

Поле preset задаёт плотность материала:

  • brief — режим по умолчанию: самодостаточный текст для клиента простым языком, один результат и только необходимые вопросы или действия, без внутренней кухни и жаргона;

  • standard — компактное структурированное сообщение;

  • detailed — полный отчёт с разделами и деталями.

Для send_update, send_text и notify_completion по умолчанию используется brief. В нём итог ограничен 180 символами, каждый пункт — 140 символами, а всё сообщение — пятью пунктами. send_text остаётся свободной формой. Перед отправкой проверяется лимит Telegram: не более 4096 отображаемых символов с учётом HTML-разметки. Автоматического разбиения длинного HTML в этой версии ещё нет.

Если нужно раскрыть вариант, процесс или функцию, skill использует send_text: название объекта, затем полный перечень необходимых шагов, свойств, результата и ограничений. Краткость в этом режиме убирает рассуждения и обобщения, но не факты.

Тема запроса служит жёсткой границей сообщения. Вопрос клиенту добавляется, только если ответ ещё неизвестен, находится у клиента и без него нельзя выполнить ближайшее действие по этой теме. Побочные проблемы проекта и вопросы о более поздних шагах не добавляются.

Перед формулировкой вопроса skill строит цепочку зависимостей и выбирает первое неизвестное решение, которым управляет клиент. Готовый текст обращается напрямую к клиенту; внутренние формулировки вроде «заводить ли заказчице доступ» запрещены. Пары примеров находятся в skills/herald-send/references/decision-examples.md.

send_client_copy является основным путём для сообщений, которые начальство пересылает клиенту. Заголовки задаются содержанием обращения или проекта, например Пункт СДЭК, Фотографии или Оплата. Сервер отклоняет фиксированные заголовки внутреннего отчёта вроде Проблемы, Нужно решить и Вопросы. Жёсткого лимита слов, тем или фактов нет: сохраняются все нужные клиенту сведения, пока сообщение помещается в лимит Telegram на 4096 видимых символов.

send_file в режиме auto отправляет небольшие JPEG/PNG/WebP как фотографию, остальное — как документ. Подпись ограничена 1024 отображаемыми символами; документ — настроенным лимитом до 50 МБ.

Ручное подключение из checkout

Этот способ нужен для разработки или установки без marketplace. Не смешивай его с установкой плагина: иначе клиент увидит две копии skill или два MCP-сервера.

git clone https://github.com/ZenonEl/herald.git
cd herald
uv sync --locked
mkdir -p ~/.config/herald
cp config.example.toml ~/.config/herald/config.toml

Используй абсолютный путь к checkout, чтобы сервер был доступен из любого проекта и нового чата.

Codex (пользовательский ~/.codex/config.toml):

codex mcp add herald -- uv run --directory /absolute/path/to/herald herald

Claude Code (важен scope user, а не default local):

claude mcp add --scope user herald -- \
  uv run --directory /absolute/path/to/herald herald

После добавления открой новые сессии. Проверка:

codex mcp get herald
claude mcp get herald

Общий skill лежит в skills/herald-send. При ручной установке обе системы могут использовать один источник через символические ссылки:

mkdir -p ~/.claude/skills ~/.agents/skills
ln -s /absolute/path/to/herald/skills/herald-send ~/.claude/skills/herald-send
ln -s /absolute/path/to/herald/skills/herald-send ~/.agents/skills/herald-send

В таком режиме без plugin namespace он вызывается как /herald-send в Claude Code и $herald-send в Codex. Инструкция предлагает применить доступный humanizer, но не требует и не копирует его: если такого скилла нет, отправка продолжается по встроенному чек-листу.

Разработка

uv run pytest

Тесты не обращаются к Telegram: HTTP-ответы подменяются локально, а MCP-контракт проверяется in-memory клиентом официального SDK.

Место в связке

Проект

Роль

mnemo

архив материала с провенансом + факты, решения, вопросы

ephemeris

дейлики: состояние и синк в issues

herald

канал наружу к людям

herald ничего не хранит. Он отправляет то, что ему дали, и не знает, откуда это взялось. Связь с остальными — только через ссылку на запись в тексте сообщения (см. ниже): отправил разбор — вставил ссылку, по которой видно, на каком материале он стоит.

Захват рабочих чатов

Вторая половина того же канала: herald не только отправляет наружу, но и вычитывает рабочие группы в локальный буфер, откуда ассистент забирает их сам — вместо ручного копирования сообщений, файлов и скриншотов.

Смысл не в удобстве. Копипаста из Telegram подписывает пересланное сообщение тем, кто его переслал; Bot API отдаёт настоящего автора, если тот не скрыл себя. На живом рабочем чате это разница в 16 сообщениях из 21.

Захват сейчас рассчитан на Unix (flock); готовый сервис — на Linux с systemd. Обычная отправка через MCP от systemd не зависит.

Что нужно от Telegram

  • бот добавлен в рабочую группу;

  • privacy-режим выключен у BotFather — иначе бот видит только команды и ответы себе. Настройка применяется лишь после переприглашения бота в группу;

  • право «отправлять сообщения» можно снять: читать это не мешает, а случайно написать в рабочий чат станет нечем.

Пределы, которые не обходятся

  • истории нет. Бот не прочитает ни одного сообщения, отправленного до того, как его добавили. Всё, что было раньше, вносится выгрузкой чата;

  • очередь живёт около суток. Простой меньше суток демон догоняет сам, дольше — сообщения потеряны. Отсюда Restart=always и heartbeat в inbox_status;

  • скачивание до 20 МБ. Больший файл записью не теряется: остаётся строка с пометкой, что скачать не удалось, и оригинал в Telegram.

Настройка захвата

Marketplace-плагин автоматически подключает MCP и skill, но не запускает фоновый процесс. Для постоянного capture нужен стабильный checkout репозитория: путь внутри plugin-cache меняется при обновлении. Клонируй Herald вручную, настрой тот же пользовательский конфиг и запускай демон из checkout.

В ~/.config/herald/config.toml (или в файле, на который указывает HERALD_CONFIG — им же удобно пробовать, не трогая боевой):

[capture]
enabled = true            # в поставляемом примере false, иначе демон откажется стартовать
ttl_days = 7
capture_self = true       # твои собственные сообщения тоже нужны: половина
                          # договорённостей звучит в твоих же ответах
# self_id = 123456789     # обязателен, только если capture_self = false

[[capture.chats]]
id = -1001234567890       # ЧИСЛО, без кавычек — в отличие от chat_id в [routes]
topic_id = 42             # необязательно: захватывать только этот топик форума
slug = "demo-shop"        # войдёт в ссылки ctx: и в имя каталога буфера

Можно перечислить несколько топиков одной группы отдельными блоками с разными topic_id и slug. Если задан topic_id, сообщения из общего чата и других топиков не сохраняются. Запись без topic_id захватывает весь чат; смешивать её с отдельными топиками того же чата конфиг не разрешит.

Секции [routes] и [projects] для захвата не нужны — конфиг может быть только читающим. А вот [platforms.telegram] с токеном нужен всегда: читать без токена нельзя, даже если отправлять нечего.

chat_id берётся из getUpdates, но демон опрашивает тот же токен, а двух опросов Telegram не допускает. Поэтому узнавай id до запуска демона или остановив его.

Запуск

uv run herald-capture --once   # один опрос и выход: проверить настройку
uv run herald-capture          # рабочий режим

Демон один: два опроса на один токен Telegram отвергает, поэтому второй экземпляр честно откажется стартовать. Лок берётся по токену, а не по базе, и лежит в ~/.local/share/herald/locks: это общий путь для systemd с PrivateTmp и ручного --once. Отправка при этом не мешает — конфликтует только опрос с опросом.

Готовый юнит — herald-capture.service в корне репозитория:

cp herald-capture.service ~/.config/systemd/user/
$EDITOR ~/.config/systemd/user/herald-capture.service
systemctl --user daemon-reload
systemctl --user enable --now herald-capture

В юните поправь под себя:

  • ExecStart — путь к клону (в файле стоит %h/GitHub/herald);

  • ReadWritePaths — если менял database или files_dir, иначе ProtectSystem=strict не даст туда писать;

  • Environment=HERALD_CONFIG=… — если конфиг лежит не по умолчанию.

Пути в конфиге меняются только с рестартом. Демон открывает базу и каталог файлов один раз; подхватывать их на лету значило бы, что он пишет в старую базу, пока ассистент читает новую и видит пустой буфер. Про смену пути в логе будет предупреждение. Всё остальное — список чатов, enabled, TTL — действует сразу.

Что видит ассистент

Команда

Что делает

inbox_status

объём буфера по чатам и heartbeat демона, без содержимого

inbox_export

основной путь: самодостаточный каталог для импорта в архив

target — обязательно новый каталог; повтор в тот же отвергается

include_taken — пересобрать пачку, отданную раньше и не дошедшую до архива

inbox_fetch

только строки, без копирования файлов — почитать текст

inbox_done

зафиксировано в архиве → удаляет скачанные копии, строка живёт TTL

Инструменты видны всегда, но при capture.enabled = false отвечают отказом.

Ответы сохраняются вместе с reply_to и структурированным reply_context: автором, датой, полным текстом/caption родителя, метаданными вложения и точной выделенной цитатой (quote). Если родитель находится в разрешённом топике, Herald добавляет его в буфер отдельной записью и скачивает доступное вложение. Так ответ на старое сообщение может донести его в архив даже если бот вступил в чат позже. Произвольно читать историю бот не может: Telegram передаёт только одного непосредственного родителя нового ответа.

Контекст из топика, которого нет в [[capture.chats]], целиком не сохраняется: остаётся только явно показанная в разрешённом сообщении цитата. Для внешнего ответа Telegram не отдаёт полный текст; Herald фиксирует доступные origin, id, метаданные вложения и цитату, но не пытается выдать их за исходное сообщение.

Как захваченное попадает в архив

inbox_export пишет каталог такого вида:

bundle/
├── inbox.json          строки сообщений; local_path — ОТНОСИТЕЛЬНЫЙ путь
└── files/<slug>/…      сами вложения

Относительность здесь не деталь: архив обязан отвергать источник, который адресует что-либо вне своего каталога, — правило существует потому, что выгрузки приходят от третьих лиц. Отдавать сырые строки с абсолютными путями значит получить все вложения в статусе «пропало» при живых файлах на диске.

Дальше — обычный импорт в mnemo:

python3 …/mnemo/scripts/mnemo_import.py --export _chat-export --source bundle
python3 …/mnemo/scripts/mnemo_import.py --export _chat-export --source bundle --apply

Строка сообщения. Обязательные поля — без любого из них архив откажется принять каталог, и это намеренно: без author_name импорт прошёл бы, подписав все сообщения как «неизвестно», а тихая порча хуже честного отказа.

Поле

Обяз.

Значение

chat_id

да

числовой id чата

message_id

да

номер сообщения; уникален внутри чата

chat_slug

да

тема; из него берётся slug экспорта

date

да

ISO-8601 с зоной

author_name

да

кто отправил (не обязательно автор — см. origin_type)

origin_type

нет → написал отправитель; user → переслано, автор известен; hidden_user / chat / channel → автор не установлен

origin_name

показанное имя; автором не считается, когда origin_type не user

origin_id, origin_date

нет

id и время оригинала пересылки

media_kind

нет

voice · photo · document · video … — задаёт зону RAW

file_id

нет

есть вложение; вместе с пустым local_path даёт запись «не добыт»

file_name, mime, size

нет

как прислал Telegram

local_path

нет

путь внутри каталога выгрузки либо null

media_note

нет

почему файла нет — попадает в архив, а не теряется

author_username, reply_to, topic_id

нет

как есть

reply_context

нет

снимок родителя/внешнего ответа и точная цитата

Один каталог — один чат: номера сообщений в разных чатах повторяются, и смешение молча теряло бы часть. inbox_export отказывается собирать пачку из нескольких чатов.

Буфер — не архив. Он неполон (нет истории, нет удалений) и живёт неделю; разбираться, что из этого нужно, — работа архива, а не буфера.

Ссылки на записи

Единый формат цитирования для всей связки:

ctx:<slug>#<id>          ctx:demo-shop#i004   ctx:meetings#q012

ctx — от «context», а не от имени инструмента. Программа, которая ведёт архив, может смениться; ссылка не должна от этого умирать. slug — тема (экспорт), id — запись внутри неё: i материал, q вопрос, t требование, r изъятие.

Нормативное описание — mnemo/SPEC/CITATION.md.

Общего у трёх проектов — три опубликованных версионированных формата: ссылка, манифест и контракт чтения. Ничего исполняемого: ни библиотеки, ни базы, ни общего процесса. Тот, кому нужны данные архива, вызывает команду mnemo и получает JSON — так же, как herald вызывает Telegram.

Лицензии

Репозиторий лицензирован по частям:

Путь

Лицензия

README.md и skills/herald-send/references/ — документация и текстовые материалы

CC BY-SA 4.0

всё остальное — сервер, capture, MCP, навык, конфиг и тесты

AGPL-3.0-or-later

Для производных редакций документации нужны указание авторства, отметка об изменениях и та же лицензия. Для изменённой сетевой версии Herald пользователям должен быть доступен соответствующий исходный код. Сообщения, файлы и другие пользовательские материалы, проходящие через Herald, этими лицензиями не перелицензируются.