Я знаю, что ты думал в прошлый дейлик
- пятница, 25 апреля 2025 г. в 00:00:06
Утро среды.
Вы медленно открываете meet/slack/rocket/etc и нажимаете на кнопку "📞"
Имена людей в групповом звонке вам давно известны. Слова, произносимые людьми вам тоже, кажется, известны. Но что-то было вами забыто, что-то очень важное, что будет так нужно вспомнить в тот момент, как очередь доберется по вашу душу.
Через окно солнце щекочет экран монитора, заставляя вас отклоняться то вправо, то влево, дабы увидеть символы на мониторе. На крутом подоконнике журчат жирные голуби, а над вами сверлит до боли {любимый} сосед.
И вот внезапно подходит ваша очередь, и, как гром среди ясного неба, звучит неожиданное: "{HeroName}, что нам расскажешь сегодня?".
Вы, пытаясь выцарапать из чертогов полусонного разума, наконец вытаскиваете из головы обрывки кода, складывая их в рваные фразы, пытаясь раздуть важность сказанного обилием уточнений и ещё более длинным списком уточнений тех самых уточнений. Ещё пару десятков минут обсуждаете надуманные проблемы с менеджерами.
Очередь переходит к следующим участникам беседы.
Фоновым процессом вы продолжаете слушать других участников карнавала. Все кастуемые заклинания других пользователей группового чата моментально стираются вашей памятью.
Наконец звук в наушниках затихает, и вы снова отправляетесь выполнять рабочие квесты.
Повторите снова.
Если быть честным, муторные обсуждения вещей, которые вполне можно было бы оговорить письменно, нехило выбивают из фокуса — независимо от времени суток. Несмотря на исследования, в которых утверждается, что дейлики полезны, большую часть времени, проведённого на дейликах, я бы не отнёс к чему-то действительно ценному или стоящему траты времени.
Посему я долго думал, как можно скрестить полезное, хайповое и приятное — и пришёл к созданию собственного MCP-сервера для самодокументирующихся пушей в Git, с помощью которых можно составлять дайджесты и использовать RAG.
Долгое время я наблюдаю, как ИИ несётся вперед — несётся отбирать у меня работу по 300к/nanosec и вручать талончик на уютную должность на заводе. Несмотря на это зрелище, я всё же не отказываюсь от благ, которые даёт мне ИИ, хоть моя психика и сопротивляется этому изо всех сил.
Пару недель назад я установил себе Cursor. До этого всё время пользовался продуктами JetBrains.
Поюзав Cursor, я параллельно начал погружаться в MCP: читать документацию, тестировать написанные сообществом серверы.
В один из дней, когда глаза уже выжгло от бесконечных [fix]
в [commit_messages]
, и последний дейлик был позади, я решил — пора писать свой MCP-сервер. Такой, который бы прибил созвоны, задушил бессмысленные названия коммитов и превратил всё это в приятный для чтения фид, понятный как менеджерам, так и разработчикам.
Чтобы внедрить практику пушей через агента в команде, нужно, чтобы execution prompt был максимально приближен к «полевым условиям» (или просто — к привычным командам и контексту).
Какие ещё должны быть преимущества у данного решения?
Проверяется, есть ли уже .git
; если нет — скрипт выполняет git init
.
Генерация сообщений коммитов с помощью LLM исходя из самого сообщения комита (в дальнейшем планируется добавить генерацию сообщения из данных внутри diff, или diffsumary, в случае если комит слишком большой).
Автоматическое определение текущей ветки, автоматически создавать master
или main
, если их нет, и ставит upstream‑связь.
Вместо набора ручных команд (git init
, git config
, git add
, git commit
, git remote add
, git push
) достаточно написать одну команду агенту:
git push main [сообщение коммита] имя_репозитория
Такой подход не будет сильно корёжить разработчика — ну, почти.
Хотя в глаза сразу бросается странное: имя_репозитория
.
Неужели каждый раз нужно указывать полное название репозитория, в который я хочу запушить?
К сожалению, AI не знает, откуда его вызывают, в каком окружении он работает и кто именно инициирует вызов — если эта информация явно не передана в запросе. Это сделано специально — из соображений безопасности, универсальности и контроля доступа. Поэтому в конфиге вы указываете абсолютный путь (или пути), где находятся все ваши репозитории.
Для отладки есть отдельная тулза — list_repositories
. С её помощью можно в чате с AI-агентом получить, а точнее отебажить список всех локально доступных репозиториев.
После пуша хочется куда-то сохранять информацию о коммите. Например, можно поднять PostgreSQL, прикрутить pgvector
, накатить индексы и сделать семантический поиск по сущностям коммитов. В целом, решение рабочее. Но стоит помнить: ягода pg не для этого росла. В таких задачах куда разумнее использовать специализированные векторные базы — они производительнее, лучше масштабируются, и уже из коробки поддерживают все необходимые ML-модули. И так далее по списку.
В статьях про векторные БД чаще всего встречается ChromaDB, реже — Weaviate, Pinecone или Qdrant (возможно, мне просто знания букв не хватает для чтения). Из спортивного интереса, солидарности со «слабыми» и потому что функционала Weaviate из коробки достаточно для MVP, я выбрал именно её. На её основе мы реализуем RAG — и для тех, кто хочет знать, кто вчера уронил продакшен, и для автоматической генерации дайджестов по коммитам прошедшего дня.
В завершение подключим уведомления в Slack: так вся команда будет в курсе всех твоих "[fix]".
Для начала разберемся, что такое MCP сервера.
MCP‑сервер — это сервис, функционально аналогичный API, предоставляющий агенту возможность взаимодействовать с внешними системами: файловой системой, поисковыми сервисами, мессенджерами и даже устройствами «умного дома» (Home Assistant).
Полный список серверов от сообщества можно найти здесь.
Ключевые понятия MCP‑системы:
GET‑запросы представлены в виде ресурсов для чтения данных;
POST, PUT, DELETE оформляются как инструменты (tools) для модификации состояния системы;
промпты в контексте MCP выступают в роли описания интерфейса клиента, аналогичного спецификации Swagger.
Для того чтобы начать создавать свой первый mcp-клиент или mcp-сервер можно использовать SDK, доступные для python, node, c#, java, kotlin. Так как я умею в ноду, и умею лучше, чем в python, то буду писать сервер на стеке node + typescript.
Устанавливаем зависимости и переходим к базе.
Для тех, кто не хочет читать, вот ссылка на сам mcp-сервер:
https://www.npmjs.com/package/@golddeity/gitdigester
Инициализируем клиент для weaviate.
import weaviate, { dataType, WeaviateClient, vectorizer, tokenization } from "weaviate-client";
const weaviateClient: WeaviateClient = await weaviate.connectToWeaviateCloud(
weaviateUrl,
{
authCredentials: new weaviate.ApiKey(config.weaviateKey || ""),
}
);
Если по соображениям безопасности вы не хотите использовать облачный Weaviate, можно развернуть образ базы и модели для эмбедингов с помощью docker compose локально. В данном случае я добавил модуль generative-mistral для RAG, так как mistral не требует пополнения счёта и можно погонять её локально бесплатно.
weaviate:
command:
- --host
- 0.0.0.0
- --port
- '8080'
- --scheme
- http
image: cr.weaviate.io/semitechnologies/weaviate:1.30.0
depends_on:
- t2v-transformers
ports:
- 8082:8080
- 50051:50051
volumes:
- weaviate_data:/var/lib/weaviate
restart: on-failure:0
environment:
QUERY_DEFAULTS_LIMIT: 25
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true'
PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
ENABLE_API_BASED_MODULES: 'true'
ENABLE_MODULES: 'text2vec-transformers,generative-mistral'
MISTRAL_APIKEY: 'kQkzna77beFXrs0q4nrrF997LACYWAGk'
TRANSFORMERS_INFERENCE_API: http://t2v-transformers:8080 # Set the inference API endpoint
CLUSTER_HOSTNAME: 'node1'
t2v-transformers: # Set the name of the inference container
image: cr.weaviate.io/semitechnologies/transformers-inference:sentence-transformers-paraphrase-multilingual-MiniLM-L12-v2
environment:
ENABLE_CUDA: 0 # Set to 1 to enable
Сетапим схе:
const setUpWeaviate = async () => {
await weaviateClient.collections.create({
name: "commits",
vectorizers: vectorizer.text2VecOpenAI(),
properties: [
{
name: "commitMessage",
dataType: dataType.TEXT,
tokenization: tokenization.LOWERCASE,
},
{
name: "commitHash",
dataType: dataType.TEXT,
},
{
name: "commitDate",
dataType: dataType.DATE,
},
{
name: "commitAuthor",
dataType: dataType.TEXT,
},
{
name: "commitBranch",
dataType: dataType.TEXT,
},
{
name: "commitDiff",
dataType: dataType.TEXT,
},
],
});
}
Для git будем использовать библиотеку simple GIT. Это более надёжное решение, чем работать с гитом через спавнеры процессов. Внутри библиотеки уже реализованы все обработки ошибок, методы возвращают результаты выполнения в структурированном виде, что избежать парсинга stdout, stderr.
Вначале обновляем URL удаленного репозитория в зависимости от параметра аутентификации указанного в параметрах конфига клиентом MCP сервера.
git = simpleGit(gitOptions);
const remotes = await git.remote(['get-url', 'origin']);
if (!remotes) return;
const remoteUrl = remotes.trim();
let newUrl = '';
Пользователь должен иметь возможность указать через аргументы тип аутентификации и путь до ключей.
"--git-auth-method=ssh","--git-ssh-key=/home/{your_pc}/.ssh/id_rsa",
if (config.gitAuthMethod === 'ssh' && remoteUrl.includes('https://')) {
try {
const httpsUrl = new URL(remoteUrl);
const host = httpsUrl.hostname;
const path = httpsUrl.pathname.replace(/^\//, '');
newUrl = `git@${host}:${path}`;
console.error(`Converting HTTPS URL to SSH: ${newUrl}`);
await git.remote(['set-url', 'origin', newUrl]);
} catch (error) {
console.error(`Error converting HTTPS to SSH URL: ${error}`);
}
} else if (config.gitAuthMethod.startsWith('https') && remoteUrl.startsWith('git@')) {
try {
const sshMatch = remoteUrl.match(/git@([^:]+):(.+)/);
if (sshMatch) {
const [, host, path] = sshMatch;
newUrl = `https://${host}/${path}`;
if (config.gitAuthMethod === 'https-token' && config.gitUsername && config.gitToken) {
const urlObj = new URL(newUrl);
urlObj.username = config.gitUsername;
urlObj.password = config.gitToken;
newUrl = urlObj.toString();
}
console.error(`Converting SSH URL to HTTPS: ${newUrl}`);
await git.remote(['set-url', 'origin', newUrl]);
}
} catch (error) {
console.error(`Error converting SSH URL to HTTPS: ${error}`);
}
} else if (config.gitAuthMethod === 'https-token' && remoteUrl.includes('https://') &&
config.gitUsername && config.gitToken) {
try {
const urlObj = new URL(remoteUrl);
if (urlObj.username !== config.gitUsername || urlObj.password !== config.gitToken) {
urlObj.username = config.gitUsername;
urlObj.password = config.gitToken;
newUrl = urlObj.toString();
console.error(`Updating HTTPS URL with credentials`);
await git.remote(['set-url', 'origin', newUrl]);
}
} catch (error) {
console.error(`Error updating HTTPS URL: ${error}`);
}
С помощью git.remote(['set-url', 'origin', newUrl])
обновляется URL удалённого репозитория.
Если в конфигурации аутентификация выбранна как 'ssh'
, но текущий URL имеет формат https://...
, тогда нужно сконвертировать его в SSH-формат.
Создаётся объект URL
для удобного извлечения hostname
(имени хоста) и pathname
(пути).
Удаляется ведущий слэш из pathname
.
Формируется новый URL вида: git@host:path
.
Выводится сообщение в консоль (через console.error
для логирования при использовании modelcontextprotocol/inspector, но об этом позже).
Если выбран режим HTTPS (или его разновидность) и текущий URL имеет формат SSH (git@...), необходимо выполнить обратное преобразование.
С помощью регулярного выражения извлекаются хост и путь.
Формируется новый URL в формате https://host/path.
Дополнительная проверка: если режим аутентификации — https-token и заданы имя пользователя и токен, то эти креденшелы добавляются в URL через объект URL (устанавливая username и password).
Логику сетапа данных для аутентификации опустим, там ничего интересного нет.
Инициализируем инструменты для агента. В первому аргументе указывает название инструмента, во втором примерный промпт, чтобы AI агент определил в какой момент ему дергать тот или иной инструмент. С помощью zod и метода describe мы подсказываем агенту как вычленять параметры из промпта.
server.tool(
"git_push_origin",
"Process git commit and push operations with enhanced commit messages",
{
branchName: z.string().describe("Branch name"),
commitData: z.string().describe("Commit message"),
repositoryName: z.string().optional().describe("Repository name (optional)"),
currentDirectory: z.string().optional().describe("Current working directory of the user (optional)"),
},
async ({ branchName, commitData, repositoryName, currentDirectory }) => {
Далее мы автоматически извлекаем URL репозитория, выбираем текст последнего коммит‑сообщения и прогоняем его через DeepSeek — чтобы получить минималистичный, но ёмкий результат без лишних затрат. В будущем мы добавим две гибкие настройки: возможность передавать собственный промпт для LLM и флаг, указывающий, нужно ли включать diff
при расширении сообщения коммита. Однако для нашего MVP MCP текущего набора функций более чем достаточно.
const repoUrl = await git.remote(['get-url', 'origin']) || '';
let userName = '', userEmail = '';
let latestCommit = '';
try {
userName = (await git.raw(['config', 'user.name'])) || '';
userEmail = (await git.raw(['config', 'user.email'])) || '';
} catch (error) {
console.error("Error getting git user info:", error);
userName = "Unknown";
userEmail = "unknown@example.com";
}
const completion = await openai.chat.completions.create({
model: "deepseek-chat",
messages: [
{
role: "system",
content: "Вы — ассистент, который расширяет и улучшает сообщения коммитов. Сделайте их более описательными и профессиональными, сохраняя исходный замысел. Переводите сообщение коммита на русский язык. Делайте это максимально коротко и понятно."
},
{
role: "user",
content: `Пожалуйста, расширьте и улучшите это сообщение коммита: "${commitData}"`
}
],
});
const enhancedCommitMessage = completion.choices[0]?.message?.content || commitData;
await git.add('.');
const commitResult = await git.commit(enhancedCommitMessage);
console.error(`Commit result:`, commitResult);
const gitDiff = await git.diff(['HEAD~1', 'HEAD']);\
При попытке запушить изменения скрипт проходит через несколько шагов:
Определение текущей ветки
Сначала мы получаем название текущей ветки (currentBranch
) и сравниваем его с целевой (branchName
). Если они совпадают, ничего делать не нужно — просто пушим изменения.
Проверка и переключение на целевую ветку
Если мы находимся не в той ветке, в которую хотим запушить:
Выводим в консоль предупреждение о несоответствии веток.
Определяем хеш последнего коммита (latestCommit = await git.revparse(['HEAD'])
).
С помощью git branch <branchName> --contains <latestCommit>
проверяем, есть ли этот коммит уже в целевой ветке:
Если да, сообщаем об этом и выходим.
Если нет — готовимся к cherry‑pick’у.
Создание или переключение на ветку
Пытаемся выполнить git.checkout(branchName)
.
Если ветка не существует, Git автоматически создаст её (в режиме checkout
без флага -b
) — или можно явно добавить -b
, чтобы было понятнее.
Чери-пик последнего коммита: Если во время применения патча возникает конфликт или пустой коммит, мы ловим ошибку и разбираем errorMessage
.
Обработка ошибок cherry‑pick’а
Пустой коммит (nothing to commit
, previous cherry-pick is now empty
, cherry-pick is already started
):
Пытаемся git cherry-pick --skip
.
Если и это не удалось — делаем git cherry-pick --abort
.
Любая другая ошибка: сразу git cherry-pick --abort
.
На каждом шаге выводим в консоль подробный лог — что пошло не так и какие команды выполнились.
Возврат на изначальную ветку
После успешного или прерванного cherry‑pick’а скрипт обязательно переключается обратно на currentBranch
(или на сохранённый originalBranch
), чтобы не оставлять пользователя в незнакомом контексте.
Сохраняем в weavite данные:
const commitsCollection = weaviateClient.collections.get("Commits");
const uuid = await commitsCollection.data.insert({
commitMessage: enhancedCommitMessage,
commitDate: new Date().toISOString(),
commitHash: latestCommit,
commitAuthor: userName,
commitEmail: userEmail,
commitBranch: branchName,
commitDiff: gitDiff,
});
8. Отправляем вебхук в slack.
if (config.slackWebhook) {
try {
await axios.post(config.slackWebhook, {
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: "New Git Commit"
}
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: `*Repository:*\n${repoUrl.trim()}`
},
{
type: "mrkdwn",
text: `*Branch:*\n${branchName}`
}
]
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: `*Author:*\n${userName.trim()} <${userEmail.trim()}>`
}
]
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*Original message:*\n${commitData}`
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*Enhanced message:*\n${enhancedCommitMessage}`
}
}
]
});
9.Возвращаем ответ агенту. Для ответов важен формат, поэтому именно в таком виде.
content: [
{
type: "text",
text: [
`✅ Commit processed successfully!`,
`Branch: ${branchName}`,
`Original message: ${commitData}`,
`Enhanced message: ${enhancedCommitMessage}`,
config.weaviateKey ? "Data saved to Weaviate." : "Weaviate integration skipped.",
config.slackWebhook ? "Notification sent to Slack." : "Slack notification skipped.",
`Note: Manual 'git push origin ${branchName}' is required to complete the operation.`
].join("\n")
}
]
После наполнения базы достаточным количеством информации о комитах, мы можем начать отправлять query и получать выжимку с помощью любой генеративной модели.
Получим объект коллекции.
const commitsCollection = weaviateClient
.collections
.get("Commits");
Выполним векторный поиск по тексту запроса.
const searchRes = await commitsCollection.query
.nearText([query], {
limit: 5,
returnProperties: ["commitMessage", "commitAuthor", "commitDate"],
returnMetadata: ["distance"],
})
.do();
Формируем контекст для модели и отправляем запрос на генерацию текста на основе полученных данных.
const commits = searchRes.data.Get.Commits; //
const context = commits
.map((c: any) => `• [${c.commitDate}] ${c.commitAuthor}: ${c.commitMessage}`)
.join("\n");
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const completion = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [
{
role: "system",
content: "Вы — ассистент, который отвечает на вопросы на основе истории коммитов.",
},
{
role: "user",
content: `Ниже список последних коммитов:\n${context}\n\nВопрос: ${query}`,
},
],
});
console.log("Ответ LLM:", completion.choices[0].message?.content);
По итогу на выходе несложная имплементация для mcp-сервера, позволяющая генерировать документацию и получать данные в человекочитаемом виде менеджерам-проектов и разработчикам.
В ближайших планах:
Добавить cron‑задачи для автоматического формирования и рассылки дайджестов по ключевым изменениям.
Реализовать гибкий tool‑интерфейс агента для запросов в Weaviate (поддержка различных фильтров, векторных и keyword‑поисков).
Ввести возможность пользовательской настройки промптов для LLM и расширить логику обработки diff‑патчей (например, конкатенация нескольких коммитов в один отчёт).