javascript

Я знаю, что ты думал в прошлый дейлик

  • пятница, 25 апреля 2025 г. в 00:00:06
https://habr.com/ru/articles/903802/

Aw sheets, here we go again

  • Утро среды.

  • Вы медленно открываете 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 server starts...

Для начала разберемся, что такое 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

Инициализируем клиент для 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

Для 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).

  • Логику сетапа данных для аутентификации опустим, там ничего интересного нет.

Tools

Инициализируем инструменты для агента. В первому аргументе указывает название инструмента, во втором примерный промпт, чтобы 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']);\

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

  1. Определение текущей ветки
    Сначала мы получаем название текущей ветки (currentBranch) и сравниваем его с целевой (branchName). Если они совпадают, ничего делать не нужно — просто пушим изменения.

  2. Проверка и переключение на целевую ветку
    Если мы находимся не в той ветке, в которую хотим запушить:

    • Выводим в консоль предупреждение о несоответствии веток.

    • Определяем хеш последнего коммита (latestCommit = await git.revparse(['HEAD'])).

    • С помощью git branch <branchName> --contains <latestCommit> проверяем, есть ли этот коммит уже в целевой ветке:

      • Если да, сообщаем об этом и выходим.

      • Если нет — готовимся к cherry‑pick’у.

  3. Создание или переключение на ветку

    • Пытаемся выполнить git.checkout(branchName).

    • Если ветка не существует, Git автоматически создаст её (в режиме checkout без флага -b) — или можно явно добавить -b, чтобы было понятнее.

  4. Чери-пик последнего коммита: Если во время применения патча возникает конфликт или пустой коммит, мы ловим ошибку и разбираем errorMessage.

  5. Обработка ошибок cherry‑pick’а

    • Пустой коммит (nothing to commit, previous cherry-pick is now empty, cherry-pick is already started):

      1. Пытаемся git cherry-pick --skip.

      2. Если и это не удалось — делаем git cherry-pick --abort.

    • Любая другая ошибка: сразу git cherry-pick --abort.
      На каждом шаге выводим в консоль подробный лог — что пошло не так и какие команды выполнились.

  6. Возврат на изначальную ветку
    После успешного или прерванного cherry‑pick’а скрипт обязательно переключается обратно на currentBranch (или на сохранённый originalBranch), чтобы не оставлять пользователя в незнакомом контексте.

  7. Сохраняем в 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")
    }
  ]

RAG

После наполнения базы достаточным количеством информации о комитах, мы можем начать отправлять 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-сервера, позволяющая генерировать документацию и получать данные в человекочитаемом виде менеджерам-проектов и разработчикам.

В ближайших планах:

  1. Добавить cron‑задачи для автоматического формирования и рассылки дайджестов по ключевым изменениям.

  2. Реализовать гибкий tool‑интерфейс агента для запросов в Weaviate (поддержка различных фильтров, векторных и keyword‑поисков).

  3. Ввести возможность пользовательской настройки промптов для LLM и расширить логику обработки diff‑патчей (например, конкатенация нескольких коммитов в один отчёт).