Skip to content

Repository files navigation

CoverCraft

Go Version License: MIT CI

Генератор сопроводительных писем с fit-оценкой на коде (не LLM), гиперперсонализацией из вашего профиля и open-source кодом под MIT. Нативное окно (WebKit2GTK) + Go-бэкенд, один бинарник, настройки в ~/.config/covercraft/settings.json, контекст кандидата в context/*.md.

Автор: Robert Yusupov — github.com/turkprogrammer · yusupov-tech.ru

Уникальные преимущества

Преимущество CoverCraft Конкуренты
Fit-оценка на коде, не на модели Вердикт apply/skip/caveats считает детерминированный матчер (internal/fit), ноль LLM-токенов по умолчанию Rezi, Jobscan — оценка через LLM или эвристика
Гиперперсонализация Письмо строится из вашего реального профиля (context/*.md), а не generic-шаблонов Enhancv, Novoresume — шаблоны с автозаполнением
Приватность Профиль и письма остаются локально; нет облака-посредника TealHQ, Skillbuild — данные в их cloud
Open-source + один бинарь Go-проект под MIT, go build даёт один файл, нет npm/yarn/pip Большинство — SaaS или тяжёлые зависимости
Два движка матчинга Детерминированный (0 токенов) и гибридный (LLM с цитатами + код-верификация) Нет аналогов

Скриншоты

CoverCraft: терминальный UI генератора сопроводительных писем

История изменений

Подробная история всех версий — в CHANGELOG.md.

Как это работает

┌────────────────────────────────┐
│  GTK-окно + WebKit2GTK 4.1     │  internal/window (CGO, ~70 строк)
│  frontend/index.html (embed)   │  terminal UI, vanilla JS
└──────────┬─────────────────────┘
           │ fetch() по относительным URL
┌──────────▼─────────────────────┐
│  HTTP на 127.0.0.1:случайный   │  internal/server
│  порт: / · /api/settings ·     │
│  /api/generate ·               │
│  /api/prompt/compose            │
└──────┬──────────────┬──────────┘
       │              │
 ~/.config/…    context/*.md + LLM (любой OpenAI-совместимый API:
 settings.json  Ollama, OpenAI, OpenRouter, vLLM, …)
  • UI: один HTML-файл без бандлеров, терминальный стиль (тёмная тема по умолчанию, светлая через prefers-color-scheme), моноширинный шрифт. Ходит по относительным URL — порт в JS не нужен.
  • Настройки: хранит Go, UI читает/пишет через /api/settings. LocalStorage не используется: случайный порт = новый origin при каждом запуске, настройки бы терялись.
  • Промпт: context/*.md (профиль, проекты — всё, что .md, по алфавиту)
    • текст вакансии → единый user-промпт; системный промпт — из настроек. Кнопка [ compose ] в панели system.prompt одним LLM-вызовом отбирает нерелевантные текущей вакансии разделы профиля и сокращает user-промпт: они вырезаются через dropSections. Невырезаемы два раздела: антигаллюцинационный «ФАКТЫ-ОГРАНИЧИТЕЛИ» и «КОНТАКТЫ» (строка контактов живёт в приватном context/, вне репозитория). Системный промпт композер НЕ переписывает: поле system.prompt — рукописный актив пользователя. Страховки: при смене вакансии старый отбор сбрасывается — вырезать по чужой вакансии нельзя; сомнения трактуются в пользу «не резать».
  • Постпроверка письма (internal/audit): после генерации письмо автоматически проверяется на типовые сбои LLM — потерянные факты профиля (Yii2/Lumen/PHPUnit/Fraud Engine и метрики), запрещённое в строке стека (фреймворки, PHPUnit/PHPStan, non-tech термины), дубли технологий между буллетами и секцией пробелов, смещённую атрибуцию метрик (в письме говорится «Go-проект», а факты — из PHPUnit), плейсхолдеры и названия ИИ-редакторов. Отдельно проверяются структурные инварианты письма: обязательная секция «Адаптация под ваш стек», строка стека с префиксом «Стек: », строка контактов, финальная строка и запрет самоуничижительного начала буллета («не работал / не применял / опыта нет»). Найденное показывается панелью «⚠ проверка письма» под результатом.
  • Автоправка (auto-fix): кнопка в панели замечаний отправляет письмо + список претензий + контекст кандидата обратно модели с правилами исправления по каждому типу замечания; исправленный текст проверяется повторно — цикл до нуля замечаний. Модель правит точечно по фактам профиля, а не пишет письмо заново. Если модель вместо правки вывела кусок профиля или промпта, такой ответ отбрасывается: письмо не перезаписывается, вердикт фита не отправляется, цикл останавливается с предупреждением.
  • LLM: любой OpenAI-совместимый endpoint. Ollama работает без ключа.
  • Стриминг: /api/generate отвечает Server-Sent Events — письмо печатается в letter.out по мере генерации модели (эффект «печатающей машинки»); в конце — событие с полным текстом, elapsedMs и предупреждениями аудита.
  • Счётчик времени: /api/generate возвращает elapsedMs — UI показывает N chars · X.Xs рядом с письмом (сколько отвечала модель).

Reasoning-модели (glm и другие)

Модели с «размышлениями» (z-ai/glm-5.x и т.п.) без настройки могут думать минутами — письмо кажется «висящим», а потом приходит context deadline exceeded. В панели api.config есть селект reasoning_effort:

  • none — отключить размышления (рекомендуется для генерации писем);
  • low / medium / high — управляемое усилие;
  • «не отправлять» — параметр не уходит в запрос (для моделей и провайдеров без поддержки, например Ollama).

Замеры на реальном провайдере: glm-5.3-free без effort — 3355 reasoning-токенов и 6+ минут на письме; с none — в разы быстрее. Таймаут задаётся в настройках (timeoutSec): по умолчанию 60 s, потолок 900 s, клиент по умолчанию 300 s.

Требования

  • Ubuntu, Linux x86_64, GTK3, WebKit2GTK 4.1.
  • Go 1.26+ для сборки.
# dev-заголовки для сборки (рантайм уже есть в Ubuntu 22.04+):
sudo apt install build-essential pkg-config libwebkit2gtk-4.1-dev

libwebkit2gtk-4.0 в Ubuntu 24.04+ больше нет — поэтому вендорить webview/webview_go нельзя, окно открывается собственной CGO-обвязкой поверх системного WebKit 4.1 (internal/window).

Сборка

go build -o covercraft .
./covercraft

Установка

Основной путь — готовый бинарник из Releases:

tar xzf covercraft_0.3.5_linux_amd64.tar.gz
./covercraft

Для запуска достаточно рантайма libwebkit2gtk-4.1-0 (есть в Ubuntu 24.04+).

Альтернатива — go install (компилирует на вашей машине, нужны dev-заголовки):

sudo apt install build-essential pkg-config libwebkit2gtk-4.1-dev
go install github.com/turkprogrammer/covercraft@latest

Бинарник окажется в ~/go/bin/. Приложение ищет context/*.md рядом с бинарником или в текущем каталоге — создайте context/ там, где будете запускать.

Использование

  1. Запустите. Откроется окно ~$ covercraft в терминальном стиле.
  2. В панели api.config укажите base URL провайдера (например https://api.apinex.bond/v1 или http://127.0.0.1:11434/v1 для Ollama), модель (например free/gemini-3.8-flash, llama3.2), ключ при необходимости, и reasoning_effort для reasoning-моделей.
  3. В system.prompt — как писать письмо (есть разумный дефолт; свой кастомный промпт вставляется вручную и живёт до перезапуска). Кнопка [ compose ] уберёт нерелевантные вакансии разделы профиля, поле при этом не трогает: [ вернуть прежний ] откатывает отбор, reset default возвращает дефолт и чистит вырезанные разделы. Селектор presets над полем переключает заготовки промптов под разные цели (письмо, ответы рекрутеру, свои): [ new preset ] открывает окно с именем и текстом — видно, что именно сохранится; [ save preset ] обновляет активный текстом поля; [ edit preset ] правит имя и текст активного пресета; [ del preset ] удаляет выбранный. Пресеты лежат в ~/.config/covercraft/presets.json и переживают перезапуск; выбор копирует текст в поле, а правки в поле сессионные — в шаблон они уходят только по save/edit. После генерации строка «в модель ушло» называет источник системного промпта (пресет / ваш / дефолтный) и его первую строку — какой промпт реально ушёл, видно сразу.
  4. Вставьте вакансию в vacancy.in, нажмите [ gen ] или Ctrl+Enter.
  5. Письмо появится в letter.out — счётчик покажет размер и время генерации (N chars · X.Xs); можно отредактировать и нажать [ copy ].
  6. Письмо можно прервать кнопкой stop в шапке или клавишей Esc; Esc также очищает результат.

Настройки сохраняются автоматически (после 400 мс тишины) и переживают перезапуск. Esc очищает результат.

Фит-панель и движки

После генерации письмо проверяется фит-матчером: требования вакансии (разобранные моделью) сопоставляются с письмом и профилем, панель показывает вердикт (apply / apply_with_caveats / skip), скор и расшифровку закрыто / с оговоркой / не закрыто.

Движок задаётся полем fitEngine в ~/.config/covercraft/settings.json:

  • "" / "deterministic" (по умолчанию) — матчер на правилах: токены, синонимы, отрицания, мосты, концепт-признаки. Ноль дополнительных LLM-вызовов, воспроизводим.
  • "llm" — гибрид: модель размечает покрытие каждого must-have цитатами, код верифицирует цитаты и отрицания и считает вердикт. Один дополнительный LLM-вызов на генерацию; при ошибке модели, таймауте или битом JSON — автоматический откат на детерминированный матчер.

Динамические концепты

Концептные требования (навыки, названные словами, а не технологиями — «опыт интеграции с платёжными процессингами») матчатся по концептам: триггер в формулировке требования + минимум два сигнала-факта в письме или профиле.

  • Из коробки работает встроенный набор концептов (DefaultConcepts).
  • При fitEngine: "llm" концепты дополнительно генерируются моделью из вашего context/*.md (один LLM-вызов при первом generate) и кэшируются в ~/.config/covercraft/concepts.json с SHA-256 хэшем профиля.
  • Кэш инвалидруется автоматически при изменении профиля: хэш не совпал — перегенерация. Файл — обычный JSON (name, trigger_terms, signals), можно править вручную: правка работает, пока профиль не менялся.
  • Нет валидного кэша и недоступна LLM (нет ключа, таймаут, битый JSON) — используется встроенный набор: поведение системы не деградирует.

Структура

main.go                  точка входа: сервер + окно
frontend/index.html     весь UI (embed)
internal/settings/      JSON-настройки, 0600, атомарная запись
internal/llm/           OpenAI-совместимый клиент /chat/completions,
                         reasoning_effort (omitempty), timeout 300s
internal/cover/          сборка user-промпта из context/*.md,
                         ProfileSections/DropSections — отбор разделов
internal/prompt/         отбор разделов профиля под вакансию
                         (POST /api/prompt/compose)
internal/fit/            фит-матчер: извлечение требований, покрытия,
                         вердикт; concepts.go — динамические концепты
                         (LLM-генерация, кэш ~/.config/covercraft/concepts.json)
internal/server/         HTTP-роутер + швы LLMFunc/LLMStreamFunc для тестов,
                         /api/generate стримит SSE: дельты + done (letter,
                         elapsedMs, warnings, fit, наблюдаемость отправки);
                         streamParams — вход одного запуска письма
internal/window/         CGO: GTK-окно + WebKit2GTK 4.1
internal/audit/          постпроверка письма: запрещённые паттерны,
                         потерянные факты, дубли, атрибуция метрик
context/                 user's context: *.md (gitignored, private)

Полный обзор для AI-агентов — llms.txt. Правила контрибуции — CONTRIBUTING.md.

API (для интеграции)

Метод Путь Что делает
GET / UI (HTML из embed)
GET /api/settings текущие настройки
POST /api/settings сохранить настройки
POST /api/generate {vacancy, systemPrompt, dropSections, letter} → SSE: delta…, done {letter, elapsedMs, warnings, fit, profileWarning, usedSystemPrompt, appliedDropSections, usedUserPromptBytes, usedProfileBytes}
POST /api/prompt/compose {vacancy} → {dropSections, reason, sections, droppedBytes, elapsedMs} — отбор разделов, системный промпт не возвращается
GET /api/presets список пресетов и activePresetId
POST /api/presets {name, systemPrompt} → создать пресет (имя ≤80 символов; длина промпта не ограничена — как у поля #systemPrompt)
PUT /api/presets {id, name?, systemPrompt?} → обновить пресет (пустые поля = не менять)
DELETE /api/presets {id} → удалить пресет; удаление активного снимает activePresetId
POST /api/presets/active {id} → активировать пресет; {id: ""} — снять привязку (кастомный промпт)

Поля события done

Поля наблюдаемости нужны, чтобы отличить намерение от факта: композер прислал dropSections, но профиль могли поправить между вызовами, и часть дропов тогда ничего не вырезает.

Поле Что значит
appliedDropSections Дропы, реально вырезанные из профиля. Может быть короче запрошенного dropSections — UI обязан показать расхождение, а не рапортовать «вырезано N».
usedUserPromptBytes Длина user-промпта, реально отправленного модели.
usedProfileBytes Длина профиля, ушедшего в фит.
usedSystemPrompt Фактически отправленный системный промпт: кастом или дефолт.

usedUserPromptBytes и usedProfileBytes сериализуются всегда, чтобы клиент отличал «0 байт» от «поле не пришло». warnings предупреждает о prompt ≥8 КБ и ≥4× от дефолта, а также о неприемлемом размере ответа — когда модель вернула кусок профиля или промпта вместо письма, в warnings попадает причина, и вердикт фита при таком ответе не отправляется.

Тесты

go test ./...

Живой интеграционный прогон (TestAFLTLiveEndToEnd, internal/fit) по умолчанию пропускается: платные вызовы к модели, нужен CC_LIVE=1 и промпт v4 по пути из CC_V4_PROMPT. Ключ никогда не берётся из исходников — только из ~/.config/covercraft/settings.json.

Юнит-тесты на все пакеты: настройки (roundtrip, битый JSON, права 0600), LLM-клиент (запрос/ответ/ошибки через httptest; reasoning_effort отправляется только когда задан), сборка промпта, HTTP-эндпоинты (включая 400/502 и elapsedMs), embed-фронтенд (копирайт, селект reasoning effort), постпроверка писем (запрещённые паттерны, потерянные факты, дубли, атрибуция метрик) и режим автоправки.

Отдельно стоит детектор эха (isPromptEcho): он ловит модель, выдавшую кусок профиля или промпта вместо письма, и не должен срабатывать на обычной содержательной правке письма. Сценарии закрыты в internal/server; на живых письмах (PHP, SRE, AI-product, fullstack) зафиксированы правые требования, которые ранее ложно уходили в missing.

Отладка

  • Сервер печатает URL в stderr: covercraft: http://127.0.0.1:PORT.
  • Логи запросов — там же (GET /, POST /api/generate).
  • GDK_BACKEND=x11 ./covercraft — окно через Xwayland (удобно для скриншотов и xprop).

Дистрибуция

Артефакты релиза собираются в dist/ (не в git). В бинарник вшивается версия модуля из тега git: собирайте после тега из чистого дерева (иначе в go version -m уедет +dirty):

Версия ниже берётся из тега — так артефакт всегда совпадает с содержимым репозитория, и пример не устаревает при следующем релизе:

V=0.4.0   # версия релиза
mkdir -p dist
go build -o covercraft .
tar czf dist/covercraft_${V}_linux_amd64.tar.gz covercraft README.md LICENSE
(cd dist && sha256sum covercraft_${V}_linux_amd64.tar.gz \
  > covercraft_${V}_linux_amd64.tar.gz.sha256)

sha256sum запускается из dist/, иначе в файл суммы попадёт путь с префиксом dist/ и проверка sha256sum -c из самого каталога не пройдёт.

context/ в архив не входит: он приватен и создаётся каждым пользователем под себя. Dev-пакеты нужны только для сборки; для запуска достаточно рантайма libwebkit2gtk-4.1-0 (есть в Ubuntu 24.04+).

License

MIT — см. LICENSE.