Skip to content

feat(plugin): auto-download binary + userConfig onboarding#149

Merged
thebtf merged 1 commit into
mainfrom
feat/plugin-auto-binary
Apr 13, 2026
Merged

feat(plugin): auto-download binary + userConfig onboarding#149
thebtf merged 1 commit into
mainfrom
feat/plugin-auto-binary

Conversation

@thebtf
Copy link
Copy Markdown
Owner

@thebtf thebtf commented Apr 13, 2026

Changes

  • ensure-binary.sh: SessionStart hook auto-downloads platform-correct binary from GitHub Releases to ${CLAUDE_PLUGIN_DATA}/bin/. Version-pinned to plugin.json version. Cached until version changes.
  • userConfig: prompts for server_url and api_token on plugin enable (zero manual env setup for MCP server)
  • .mcp.json: ${CLAUDE_PLUGIN_DATA}/bin/engram binary path + user_config.* env vars
  • hooks.json: ensure-binary.sh runs BEFORE session-start.js in SessionStart chain

Onboarding flow (user perspective)

  1. /plugin install engram
  2. Claude Code asks: "Enter Engram server URL" → "Enter API token"
  3. First session: binary downloaded (~11MB, ~5 seconds)
  4. MCP tools available immediately

Test plan

  • Fresh install: binary downloads on first session
  • Second session: no download (version match)
  • Version bump: re-downloads on next session
  • Linux/macOS/Windows platform detection

Summary by CodeRabbit

Примечания к выпуску

  • Новые функции

    • Добавлены поля конфигурации для URL сервера и API токена
  • Обновления

    • Версия плагина обновлена на 4.0.1
    • Изменена система запуска сервера
    • Добавлена автоматическая проверка и управление компонентами при инициализации сессии

@gemini-code-assist
Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

- ensure-binary.sh: SessionStart hook downloads correct platform binary
  from GitHub Releases to CLAUDE_PLUGIN_DATA/bin/, version-pinned to
  plugin.json version. Atomic replace, cached until version changes.
- userConfig: prompts for server_url and api_token on plugin install
  (no manual env var setup needed for MCP server)
- .mcp.json: uses CLAUDE_PLUGIN_DATA binary path + userConfig env vars
- Plugin version 4.0.1
@thebtf thebtf force-pushed the feat/plugin-auto-binary branch from 502eca8 to 2dc7834 Compare April 13, 2026 21:48
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 13, 2026

Обзор

Плагин refactored для динамической загрузки и управления бинарным файлом engram через Node.js-скрипты. Версия обновлена до 4.0.1, добавлена конфигурация пользователя для URL сервера и токена API, интегрирована автоматическая проверка и установка бинарного файла при запуске сессии.

Изменения

Когорта / Файл(ы) Сводка
Конфигурация плагина
plugin/engram/.claude-plugin/plugin.json, plugin/engram/.mcp.json
Версия обновлена с 4.0.0 на 4.0.1. Добавлены поля userConfig для server_url и api_token. MCP сервер переконфигурирован для запуска через Node.js обёртку run-engram.js с переменными окружения из конфига пользователя.
Конфигурация хуков
plugin/engram/hooks/hooks.json
В pipeline SessionStart добавлена новая команда хука для запуска ensure-binary.js с тайм-аутом 60 секунд перед существующим хуком.
Управление бинарным файлом
plugin/engram/scripts/ensure-binary.js, plugin/engram/scripts/run-engram.js
Два новых вспомогательных скрипта: ensure-binary.js загружает и кэширует нужную версию engram с GitHub (с поддержкой кросс-платформности), run-engram.js выполняет кэшированный бинарный файл с аргументами CLI.

Диаграммы последовательности

sequenceDiagram
    participant Claude as Claude Code Plugin
    participant EnsureBin as ensure-binary.js
    participant FS as Файловая система
    participant GitHub as GitHub Releases
    
    Claude->>EnsureBin: Запуск при SessionStart (тайм-аут 60s)
    EnsureBin->>EnsureBin: Прочитать версию из plugin.json
    EnsureBin->>FS: Проверить наличие бинарного файла и .version
    alt Бинарный файл не существует или устарел
        EnsureBin->>EnsureBin: Определить платформу/архитектуру
        EnsureBin->>GitHub: Загрузить нужный релиз (HTTP/HTTPS с редиректами)
        GitHub-->>EnsureBin: Бинарный файл
        EnsureBin->>FS: Записать временный файл (.tmp)
        EnsureBin->>FS: Атомарно заменить существующий бинарный файл
        EnsureBin->>FS: Записать .version файл
        EnsureBin->>FS: Установить права на выполнение (non-Windows)
    else Бинарный файл актуален
        EnsureBin->>EnsureBin: Пропустить загрузку
    end
    EnsureBin-->>Claude: Завершить (ошибки обработаны как non-fatal)
Loading
sequenceDiagram
    participant MCP as MCP Configuration
    participant RunEngram as run-engram.js
    participant FS as Файловая система
    participant Engram as engram binary
    participant User as Пользовательский процесс
    
    MCP->>RunEngram: Запуск через node с переменными окружения
    RunEngram->>RunEngram: Прочитать CLAUDE_PLUGIN_DATA
    RunEngram->>FS: Определить путь к бинарному файлу (с расширением .exe на Windows)
    RunEngram->>FS: Проверить существование файла
    alt Бинарный файл найден
        RunEngram->>Engram: Запустить процесс с CLI аргументами
        Engram->>User: Выполнить операцию (унаследованные stdin/stdout)
        User-->>Engram: Результат выполнения
        Engram-->>RunEngram: Код выхода
        RunEngram-->>MCP: Передать код выхода
    else Бинарный файл не найден
        RunEngram->>RunEngram: Вывести ошибку с инструкцией
        RunEngram-->>MCP: Выход с кодом 1
    end
Loading

Оценка сложности кода

🎯 4 (Сложный) | ⏱️ ~45 минут

Стихотворение

🐰 Версия прыг-скок до 4.0.1!
Конфиги танцуют, хуки поют,
Бинарный файлик качается с GitHub,
Node.js ловко его берёт и хранит,
А session.start уже сверху следит! 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: auto-download binary functionality and userConfig onboarding, which are the core objectives of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/plugin-auto-binary

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugin/engram/.mcp.json`:
- Around line 7-10: Remove the hardcoded insecure flag and make ENGRAM_INSECURE
opt-in: stop unconditionally setting "ENGRAM_INSECURE": "1" in .mcp.json;
instead populate ENGRAM_INSECURE only when the user explicitly enables an
insecure option (e.g., user_config.insecure === true) or when validating that
ENGRAM_URL is a non-HTTPS scheme (starts with "http://") and the user has
consented; reference the existing keys ENGRAM_URL and ENGRAM_API_TOKEN and
implement the change where env is built so the token is not sent alongside an
implicitly enabled insecure flag.

In `@plugin/engram/scripts/ensure-binary.js`:
- Around line 81-85: The downloaded executable (tmpPath) must be
integrity-verified before being used: fetch the expected checksum or signature
for the release (e.g., a .sha256 asset or detached signature from the same
GitHub release) and verify the downloaded file after download(url, tmpPath) by
computing its SHA-256 (using crypto) and comparing to the expected value, or by
verifying the signature with the release public key; if verification fails,
remove tmpPath and throw an error so the binary is never moved/used. Locate the
download call and variables url, tmpPath, desiredVersion, suffix, and binaryPath
to implement the verification step and proper cleanup on failure.
- Around line 46-55: The platform/arch mapping currently falls back to amd64 for
any arch that's not arm64, which will download incompatible binaries for
ia32/arm/Windows ARM; update the logic in ensure-binary.js to only map known
supported triples (e.g., for darwin: arch === "arm64" -> "darwin-arm64", arch
=== "x64" -> "darwin-amd64"; for linux: arch === "arm64" -> "linux-arm64", arch
=== "x64" -> "linux-amd64"; for win32: allow only arch === "x64" ->
"windows-amd64.exe" and arch === "arm64" -> "windows-arm64.exe" if you ship it)
and otherwise write an explicit unsupported platform/arch message using
process.stderr.write and return; change the variables referenced (platform,
arch, suffix, binaryName) and the conditional blocks that currently set
suffix/binaryName to implement these strict checks.
- Around line 94-100: The current replace in ensure-binary.js deletes binaryPath
before ensuring tmpPath is moved, risking loss if fs.renameSync(tmpPath,
binaryPath) fails; change to an atomic swap with rollback: first rename
binaryPath to a backup name (e.g., backupPath = binaryPath + '.bak') only if it
exists, then rename tmpPath to binaryPath; if the second rename fails, restore
the backup by renaming backupPath back to binaryPath and surface the original
error; on success remove the backup. Use the existing variables binaryPath and
tmpPath and perform all operations inside the try/catch so failures trigger the
rollback.

In `@plugin/engram/scripts/run-engram.js`:
- Around line 26-32: The spawnSync call may return a result with result.error
instead of throwing; after calling spawnSync (in run-engram.js where spawnSync
and binaryPath are used) check if result.error is present, log the error (e.g.,
console.error with a clear message and the error.stack or message) and exit with
a non-zero code (process.exit(1)); otherwise continue to use
process.exit(result.status ?? 0) for normal termination. Ensure the check runs
before calling process.exit so failed process spawn is diagnosed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 23f3defb-557b-45c2-bc1e-5169539dd642

📥 Commits

Reviewing files that changed from the base of the PR and between 12603e3 and 2dc7834.

📒 Files selected for processing (5)
  • plugin/engram/.claude-plugin/plugin.json
  • plugin/engram/.mcp.json
  • plugin/engram/hooks/hooks.json
  • plugin/engram/scripts/ensure-binary.js
  • plugin/engram/scripts/run-engram.js

Comment thread plugin/engram/.mcp.json
Comment on lines +7 to +10
"env": {
"ENGRAM_URL": "${user_config.server_url}",
"ENGRAM_API_TOKEN": "${user_config.api_token}",
"ENGRAM_INSECURE": "1"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Не включайте ENGRAM_INSECURE без явного opt-in.

На Line 10 небезопасный режим принудительно включается для всех установок, включая случаи с обычным HTTPS endpoint. Это особенно рискованно, потому что в тот же процесс передаётся ENGRAM_API_TOKEN. Лучше выводить этот флаг из отдельной пользовательской настройки или включать его только для явно разрешённых http:///self-signed сценариев.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/engram/.mcp.json` around lines 7 - 10, Remove the hardcoded insecure
flag and make ENGRAM_INSECURE opt-in: stop unconditionally setting
"ENGRAM_INSECURE": "1" in .mcp.json; instead populate ENGRAM_INSECURE only when
the user explicitly enables an insecure option (e.g., user_config.insecure ===
true) or when validating that ENGRAM_URL is a non-HTTPS scheme (starts with
"http://") and the user has consented; reference the existing keys ENGRAM_URL
and ENGRAM_API_TOKEN and implement the change where env is built so the token is
not sent alongside an implicitly enabled insecure flag.

Comment on lines +46 to +55
if (platform === "win32") {
suffix = "windows-amd64.exe";
binaryName = "engram.exe";
} else if (platform === "darwin") {
suffix = arch === "arm64" ? "darwin-arm64" : "darwin-amd64";
} else if (platform === "linux") {
suffix = arch === "arm64" ? "linux-arm64" : "linux-amd64";
} else {
process.stderr.write(`[engram] unsupported platform: ${platform}\n`);
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Не подменяйте неподдерживаемые архитектуры на amd64.

Сейчас любой process.arch, кроме arm64, на macOS/Linux уходит в *-amd64, а Windows всегда получает windows-amd64.exe. На ia32/arm/Windows ARM это скачает несовместимый бинарник вместо явного отказа.

💡 Пример правки
   if (platform === "win32") {
-    suffix = "windows-amd64.exe";
-    binaryName = "engram.exe";
+    if (arch === "x64") {
+      suffix = "windows-amd64.exe";
+      binaryName = "engram.exe";
+    } else {
+      process.stderr.write(`[engram] unsupported architecture: ${platform}/${arch}\n`);
+      return;
+    }
   } else if (platform === "darwin") {
-    suffix = arch === "arm64" ? "darwin-arm64" : "darwin-amd64";
+    if (arch === "arm64") suffix = "darwin-arm64";
+    else if (arch === "x64") suffix = "darwin-amd64";
+    else {
+      process.stderr.write(`[engram] unsupported architecture: ${platform}/${arch}\n`);
+      return;
+    }
   } else if (platform === "linux") {
-    suffix = arch === "arm64" ? "linux-arm64" : "linux-amd64";
+    if (arch === "arm64") suffix = "linux-arm64";
+    else if (arch === "x64") suffix = "linux-amd64";
+    else {
+      process.stderr.write(`[engram] unsupported architecture: ${platform}/${arch}\n`);
+      return;
+    }
   } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (platform === "win32") {
suffix = "windows-amd64.exe";
binaryName = "engram.exe";
} else if (platform === "darwin") {
suffix = arch === "arm64" ? "darwin-arm64" : "darwin-amd64";
} else if (platform === "linux") {
suffix = arch === "arm64" ? "linux-arm64" : "linux-amd64";
} else {
process.stderr.write(`[engram] unsupported platform: ${platform}\n`);
return;
if (platform === "win32") {
if (arch === "x64") {
suffix = "windows-amd64.exe";
binaryName = "engram.exe";
} else {
process.stderr.write(`[engram] unsupported architecture: ${platform}/${arch}\n`);
return;
}
} else if (platform === "darwin") {
if (arch === "arm64") suffix = "darwin-arm64";
else if (arch === "x64") suffix = "darwin-amd64";
else {
process.stderr.write(`[engram] unsupported architecture: ${platform}/${arch}\n`);
return;
}
} else if (platform === "linux") {
if (arch === "arm64") suffix = "linux-arm64";
else if (arch === "x64") suffix = "linux-amd64";
else {
process.stderr.write(`[engram] unsupported architecture: ${platform}/${arch}\n`);
return;
}
} else {
process.stderr.write(`[engram] unsupported platform: ${platform}\n`);
return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/engram/scripts/ensure-binary.js` around lines 46 - 55, The
platform/arch mapping currently falls back to amd64 for any arch that's not
arm64, which will download incompatible binaries for ia32/arm/Windows ARM;
update the logic in ensure-binary.js to only map known supported triples (e.g.,
for darwin: arch === "arm64" -> "darwin-arm64", arch === "x64" ->
"darwin-amd64"; for linux: arch === "arm64" -> "linux-arm64", arch === "x64" ->
"linux-amd64"; for win32: allow only arch === "x64" -> "windows-amd64.exe" and
arch === "arm64" -> "windows-arm64.exe" if you ship it) and otherwise write an
explicit unsupported platform/arch message using process.stderr.write and
return; change the variables referenced (platform, arch, suffix, binaryName) and
the conditional blocks that currently set suffix/binaryName to implement these
strict checks.

Comment on lines +81 to +85
const url = `https://github.com/${REPO}/releases/download/v${desiredVersion}/engram-${suffix}`;
const tmpPath = binaryPath + ".tmp";

try {
await download(url, tmpPath);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Скачанный executable нужно проверять по checksum или подписи.

Здесь hook автоматически скачивает бинарник, который затем будет исполнен в следующем шаге. Сейчас доверие строится только на HTTPS и redirect'ах GitHub; без SHA-256/подписей компрометация release asset превращается в тихое выполнение произвольного кода при SessionStart.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/engram/scripts/ensure-binary.js` around lines 81 - 85, The downloaded
executable (tmpPath) must be integrity-verified before being used: fetch the
expected checksum or signature for the release (e.g., a .sha256 asset or
detached signature from the same GitHub release) and verify the downloaded file
after download(url, tmpPath) by computing its SHA-256 (using crypto) and
comparing to the expected value, or by verifying the signature with the release
public key; if verification fails, remove tmpPath and throw an error so the
binary is never moved/used. Locate the download call and variables url, tmpPath,
desiredVersion, suffix, and binaryPath to implement the verification step and
proper cleanup on failure.

Comment on lines +94 to +100
// Atomic replace
try {
if (fs.existsSync(binaryPath)) fs.unlinkSync(binaryPath);
fs.renameSync(tmpPath, binaryPath);
} catch (err) {
process.stderr.write(`[engram] install failed: ${err.message}\n`);
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Замена бинарника сейчас не атомарна и может удалить рабочую версию.

На Line 96 текущий бинарник удаляется до того, как новый гарантированно оказался на месте. Если renameSync на Line 97 падает, плагин остаётся вообще без бинарника. Нужен хотя бы rollback на предыдущий файл или схема, где last-known-good не удаляется до успешной установки.

💡 Более безопасный вариант с rollback
   // Atomic replace
+  const backupPath = binaryPath + ".bak";
   try {
-    if (fs.existsSync(binaryPath)) fs.unlinkSync(binaryPath);
+    if (fs.existsSync(binaryPath)) fs.renameSync(binaryPath, backupPath);
     fs.renameSync(tmpPath, binaryPath);
+    if (fs.existsSync(backupPath)) fs.unlinkSync(backupPath);
   } catch (err) {
+    try {
+      if (!fs.existsSync(binaryPath) && fs.existsSync(backupPath)) {
+        fs.renameSync(backupPath, binaryPath);
+      }
+    } catch {}
     process.stderr.write(`[engram] install failed: ${err.message}\n`);
     return;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Atomic replace
try {
if (fs.existsSync(binaryPath)) fs.unlinkSync(binaryPath);
fs.renameSync(tmpPath, binaryPath);
} catch (err) {
process.stderr.write(`[engram] install failed: ${err.message}\n`);
return;
// Atomic replace
const backupPath = binaryPath + ".bak";
try {
if (fs.existsSync(binaryPath)) fs.renameSync(binaryPath, backupPath);
fs.renameSync(tmpPath, binaryPath);
if (fs.existsSync(backupPath)) fs.unlinkSync(backupPath);
} catch (err) {
try {
if (!fs.existsSync(binaryPath) && fs.existsSync(backupPath)) {
fs.renameSync(backupPath, binaryPath);
}
} catch {}
process.stderr.write(`[engram] install failed: ${err.message}\n`);
return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/engram/scripts/ensure-binary.js` around lines 94 - 100, The current
replace in ensure-binary.js deletes binaryPath before ensuring tmpPath is moved,
risking loss if fs.renameSync(tmpPath, binaryPath) fails; change to an atomic
swap with rollback: first rename binaryPath to a backup name (e.g., backupPath =
binaryPath + '.bak') only if it exists, then rename tmpPath to binaryPath; if
the second rename fails, restore the backup by renaming backupPath back to
binaryPath and surface the original error; on success remove the backup. Use the
existing variables binaryPath and tmpPath and perform all operations inside the
try/catch so failures trigger the rollback.

Comment on lines +26 to +32
try {
const { spawnSync } = require("child_process");
const result = spawnSync(binaryPath, process.argv.slice(2), {
stdio: "inherit",
env: process.env,
});
process.exit(result.status ?? 1);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In Node.js child_process.spawnSync, when the child process cannot be started, does it throw or return a result object with an error property?

💡 Result:

In Node.js child_process.spawnSync, when the child process cannot be started (e.g., command not found, resulting in ENOENT), it returns a result object that includes an error property, rather than throwing an exception. The result object has fields like error (populated with the spawn failure Error), status: null, signal: null, output: null, stdout: null, stderr: null, and pid (a number).

Citations:


Добавьте проверку result.error после spawnSync.

Когда дочерний процесс не может быть запущен, spawnSync возвращает объект с свойством error, а не выбрасывает исключение. Текущий код не проверяет эту ошибку и просто завершается с кодом 1 без диагностики.

Пример правки
 try {
   const { spawnSync } = require("child_process");
   const result = spawnSync(binaryPath, process.argv.slice(2), {
     stdio: "inherit",
     env: process.env,
   });
+  if (result.error) {
+    process.stderr.write(`[engram] exec failed: ${result.error.message}\n`);
+    process.exit(1);
+  }
   process.exit(result.status ?? 1);
 } catch (err) {
   process.stderr.write(`[engram] exec failed: ${err.message}\n`);
   process.exit(1);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/engram/scripts/run-engram.js` around lines 26 - 32, The spawnSync call
may return a result with result.error instead of throwing; after calling
spawnSync (in run-engram.js where spawnSync and binaryPath are used) check if
result.error is present, log the error (e.g., console.error with a clear message
and the error.stack or message) and exit with a non-zero code (process.exit(1));
otherwise continue to use process.exit(result.status ?? 0) for normal
termination. Ensure the check runs before calling process.exit so failed process
spawn is diagnosed.

@thebtf thebtf merged commit 539134a into main Apr 13, 2026
2 checks passed
@thebtf thebtf deleted the feat/plugin-auto-binary branch April 13, 2026 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant