Compare commits

...

10 commits

Author SHA1 Message Date
Parfii-bot
5b8e066888 refactor(install): production-ready финальный круг
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-router,kei-sage,kei-task,kei-chat-store,kei-crossdomain,kei-search-core,kei-content-store,kei-social-store,kei-curator,kei-auth,kei-artifact name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
1. lib-onboarding.sh раскидан на 3 куба (Constructor Pattern <200 LOC):
   - lib-onboarding-registry.sh (79 LOC) — парсеры providers/models.toml
     + onboarding_fallback_providers (14 провайдеров)
     + onboarding_auth_env_for_provider helper (был inline в collect_auth)
   - lib-onboarding-ui.sh (189 LOC) — pick_language/transport/provider/model
     + collect_auth (whiptail/bash select)
   - lib-onboarding-state.sh (57 LOC) — write_secrets + write_config
     + user-model-override.toml для kei-model-router
   - lib-onboarding.sh (95 LOC) — тонкий оркестратор: should_run + run

   Сам lib-onboarding.sh source'ит 3 подкуба автоматически. Глобалы
   (ONBOARDING_*, REGISTRY_*, ONBOARDED_FLAG, etc.) объявлены в
   оркестраторе, подкубы их используют через имена.

2. lib-menu.sh локализован:
   - whiptail title + radiolist prompt через ${STR_MENU_TITLE} +
     ${STR_MENU_SUBSTRATE} + ${STR_MENU_PROFILE_PROMPT}.
   - Plain heading тоже использует словарь.
   - 12 коротких имён профилей (minimal/core/dev/...) — оставлены EN
     как стабильные id (не переводятся).

3. _blocks/build-index.sh — детерминированная регенерация INDEX.md.
   Группировка по 14 категорийным префиксам + "Прочие" для остальных.
   Безопасно перезапускать. INDEX.md обновлён через этот скрипт
   (минимальный diff — добавлена ссылка на build-index.sh в шапке).

Проверено: bash -n чисто, unit тесты onboarding_list_providers/
transports/models OK, non-TTY smoke ./install.sh --profile=minimal
--no-execute проходит.
2026-05-17 23:57:23 +08:00
Parfii-bot
5327befef6 docs(_blocks): INDEX.md — реестр 84 блоков для assembler
Авто-сгенерирован из _blocks/*.md по категориям префикса:
  API / AUTH / CI / DB / DEPLOY / DOCS / DOMAIN / MODE / OBS /
  PATH / RULE / SCRAPER / SECURITY / STACK / TEST + прочие.

Каждая запись: `name` — первая H1-строка файла.

Использование в _manifests/<agent>.toml:
  blocks = ["baseline", "rule-pre-dev-gate", "api-anthropic", ...]

Assembler читает блоки из _blocks/, склеивает в финальный
_generated/<agent>.md. Новый блок = просто .md в _blocks/.
Регенерация INDEX.md — TODO build-index.sh скрипт (сейчас
сгенерирован вручную bash циклом).
2026-05-17 20:39:06 +08:00
Parfii-bot
33f1376ee1 feat(i18n): расширил язык-набор с 2 до 16
Новые словари:
  uk    Українська
  de    Deutsch
  fr    Français
  es    Español
  pt    Português
  it    Italiano
  tr    Türkçe
  ar    العربية
  hi    हिन्दी
  zh    简体中文
  ja    日本語
  ko    한국어
  id    Bahasa Indonesia
  vi    Tiếng Việt

Каждый файл — 17 STR_* ключей (тот же контракт что en.sh + ru.sh).
lib-i18n.sh::i18n_available_languages — единый список (en + 15)
для меню выбора в мастере. i18n_load_lang упрощён: всегда грузит
английский как fallback, потом поверх — словарь языка (отсутствующий
ключ остаётся английским).

onboarding_pick_language теперь генерирует whiptail/bash select из
i18n_available_languages динамически — добавление нового языка =
один файл `install/i18n/<код>.sh` + одна строка в available_languages,
дальше всё подхватится автоматически.

Перевод формальный, без излишеств. Welcome баннер всегда EN (юзер
ещё не выбрал на момент показа).

Проверено: bash -n всех 16 словарей чисто, roundtrip всех языков
работает (i18n_load_lang en/ru/uk/de/fr/es/pt/it/tr/ar/hi/zh/ja/ko/id/vi
выдают локализованные STR_DONE_TITLE + STR_TR_DIRECT_API), non-TTY
smoke install --no-execute проходит.
2026-05-17 16:51:44 +08:00
Parfii-bot
305140f20b fix(install): close MEDIUM/LOW from RULE 0.26 audit
- preflight failure handling: вместо `|| true` (молчаливое продолжение
  при упавшем preflight) — явный prompt «продолжить? [y/N]» с return 1
  при отказе. Без TTY печатает warning и продолжает. Это закрывает
  HIGH bug-9: «.onboarded флаг выставляется при нерабочей конфигурации».

- lib-preflight.sh::preflight_check_cli — общий helper (command -v +
  offer-install + version echo). Убирает 6-file boilerplate (хотя сами
  per-provider файлы пока не переписаны под него — это отдельный шаг).

- onboarding_fallback_providers: расширен с 3 до 14 провайдеров,
  покрывает все 7 транспортов. Был дрейф vs providers.toml (14 vs 3),
  юзер без submodule видел только anthropic+openai+ollama.

- STR_PICK_PROVIDER plural mismatch: whiptail и plain ветки теперь
  используют один fallback "Provider within" (раньше plain имел
  "Providers within", whiptail — "Provider within").

- STR_DONE_NEXT удалён из en.sh + ru.sh (мёртвый ключ).

- Новые ключи: STR_MENU_* (для lib-menu.sh) + STR_PREFLIGHT_FAILED +
  STR_PREFLIGHT_CONTINUE. lib-menu.sh начал использовать
  STR_MENU_TITLE / STR_MENU_SUBSTRATE (частичная локализация, остальное
  меню — отдельной задачей).

Тесты: bash -n чисто, i18n round-trip EN/RU работает, non-TTY smoke
install --no-execute проходит.
2026-05-17 16:37:28 +08:00
Parfii-bot
a3ffaed374 fix(install,router): close 5 HIGH audit findings
1. HIGH-1: onboarding ↔ kei-model-router связка
   До: onboarding мастер писал ~/.claude/config/onboarding.toml,
   но router его не читал — выбор провайдера декоративный.
   После: lib-onboarding.sh::onboarding_write_config доп. пишет
   ~/.claude/config/user-model-override.toml; registry.rs::Registry
   получил load_user_override() возвращающий UserModelOverride.
   Приоритет: --pinned > user-override > agent-profiles default_model_ref.
   2 новых теста (round-trip TOML, optional transport).

2. HIGH-2: eval "$install_cmd" → bash -c "$install_cmd"
   До: lib-preflight.sh::preflight_offer_install делал eval.
   После: bash -c с явным subshell + печать команды юзеру до запуска.

3. HIGH-3: codex.sh regex false-pass
   До: grep -qiE "logged.in|active" пропускал "not logged in" как pass.
   После: сначала negative-pattern (not logged|signed out|please log in),
   потом positive (\blogged in\b|status: active|auth: yes).

4. HIGH-4: path traversal в source preflight
   До: lib-preflight.sh::preflight_run делал source без валидации
   provider id — `../../../evil` сработал бы.
   После: whitelist regex ^[a-z0-9][a-z0-9_-]{0,63}$ + realpath
   проверка что resolved путь не вышел за PREFLIGHT_DIR.

5. HIGH-5: curl|sh без verification
   ollama-local.sh + google-vertex.sh теперь печатают предупреждение
   что Linux-установка тянет shell-скрипт с внешнего сервера без
   проверки хэша/подписи, и предлагают альтернативу.

MEDIUM попутно:
   - anthropic-bedrock.sh: один вызов aws sts get-caller-identity
     вместо двух (экономит 1-3с), различает cred-error от network
     по тексту stderr, маскирует account ID в ARN перед печатью.
   - mlx-local.sh: pip install --user mlx-lm вместо global pip install
     (не требует sudo, не загрязняет system Python).

Тесты: cargo test --lib 80/80, bash -n всех изменённых файлов чисто.
2026-05-17 16:28:33 +08:00
Parfii-bot
0a8c93561f feat(install): preflight модуль — проверка CLI по выбранному провайдеру
Добавлен шаг между выбором модели и сбором ключей: для провайдеров
требующих внешний CLI/daemon — проверка наличия, инструкция по
установке, опциональный авто-install (TTY only).

install/lib-preflight.sh — диспетчер:
  preflight_run <provider-id>
  - ищет install/preflight/<id>.sh, source'ит, вызывает
    preflight_check_<sanitized_id>
  - функция возвращает 0/1, печатает инструкцию в stderr
  - non-TTY: только печать, без вопросов

preflight_offer_install <cli> <install-cmd>:
  - TTY: спрашивает [y/N/skip], выполняет install-cmd
  - non-TTY: печатает и пропускает

install/preflight/ — 6 файлов (только для провайдеров с CLI):
  anthropic-bedrock.sh  — aws CLI + sts get-caller-identity
  google-vertex.sh      — gcloud CLI + project config
  codex.sh              — codex CLI (npm) + login status
  ollama-local.sh       — ollama binary + 127.0.0.1:11434 daemon
  mlx-local.sh          — mlx_lm.server (arm64 only) + 127.0.0.1:8080
  lmstudio-local.sh     — порт 127.0.0.1:1234 (desktop app)

Direct-api провайдеры (anthropic, openai, xai, deepseek, google) +
proxy (litellm, openrouter) + openai-azure — preflight-файла нет,
диспетчер тихо пропускает, ключ собирается обычно.

Тесты: bash -n чисто на всех 8 файлах, unit dispatcher показывает
silent-pass для anthropic, warn+exit-1 для bedrock без aws на PATH.
2026-05-17 15:57:54 +08:00
Parfii-bot
ab260f429e feat(install): i18n модуль + welcome banner
Структура локализации:
  install/i18n/en.sh    — английский словарь (дефолт, fallback)
  install/i18n/ru.sh    — русский словарь
  install/lib-i18n.sh   — лоадер + welcome banner

Поток:
  1. install.sh source'ит lib-i18n.sh и зовёт i18n_load_default →
     все строки на английском.
  2. Если onboarding нужен — печатается welcome banner ASCII-рамка
     на английском (язык ещё не выбран).
  3. onboarding_pick_language — единственный двуязычный шаг
     ("Choose language / Выберите язык"). По выбору вызывает
     i18n_load_lang ru|en — перегружает словарь.
  4. Все последующие шаги (transport / provider / model / auth /
     completion) идут на выбранном языке.

Fallback: если ru-словарь не имеет ключа — используется английское
значение (load_default вызывается до загрузки ru.sh, переменные
перезаписываются поверх).

lib-onboarding.sh переведён со смешанных hardcoded строк на
${STR_*} placeholders.

Тесты: bash -n всех 5 файлов чисто, i18n loader unit-тест показывает
EN/RU перегрузку, non-TTY smoke install --no-execute проходит.
2026-05-17 15:35:10 +08:00
Parfii-bot
c844524f68 fix(kei-buddy): close 3 HIGH audit findings from session multi-critic swarm
1. OID-check в parse_x25519_pkcs8_pem
   До: брался последний 32-байтный slice любого PKCS#8 DER, OID не
   проверялся. RSA/EC/Ed25519 ключ молча давал 32 неправильных байта
   → decrypt падал с generic "wrong key" без объяснения.
   После: строгая проверка длины (48 байт) + OID 1.3.101.110 (X25519,
   byte slice 9..12 = 0x2b,0x65,0x6e). Внешний openssl ключ другого
   алгоритма теперь даёт явную ошибку с указанием реального OID.
   Константы X25519_OID + X25519_PKCS8_DER_LEN.
   RFC 8410 §3 + §7 ссылка в doc-комментарии.

2. x25519-dalek feature `zeroize`
   До: features=["static_secrets"] — StaticSecret хранил priv-ключ
   в куче без затирания при Drop. Локальный priv_raw.zeroize() стирал
   только стек-копию, оригинал в куче оставался до GC.
   После: features=["static_secrets","zeroize"] — StaticSecret сам
   реализует ZeroizeOnDrop, ключ затирается при выходе из scope.

3. Два новых теста:
   - parse_rejects_wrong_length_der — 32-байтный DER (вместо 48)
     отклоняется с сообщением про "48 bytes"
   - parse_rejects_wrong_oid — DER с OID Ed25519 (0x2b,0x65,0x70)
     отклоняется с сообщением про "X25519"

   8/8 тестов модуля проходят, cargo check workspace чисто.

Старая 0.14.5 mcp-server (с source maps содержавшими /Users/
denisparfionovich/...) удалена с keigit.com отдельной операцией
через Forgejo DELETE API.
2026-05-17 13:41:18 +08:00
Parfii-bot
9c6df65ae2 feat(install): onboarding wizard — transport→provider→model→keys
Новый интерактивный мастер при первой установке:
  1. Язык интерфейса (RU/EN)
  2. Транспорт (direct-api / aws-bedrock / azure-openai / google-vertex
                / local / proxy / subscription)
  3. Провайдер внутри транспорта (14 вариантов суммарно)
  4. Модель из выбранного провайдера (3 моделей Anthropic, и т.д.)
  5. Ключи/креды (silent read, пишет в ~/.claude/secrets/.env chmod 600)

Skip-логика:
  - флаг ~/.claude/.onboarded
  - env KEISEI_SKIP_ONBOARD=1
  - не-TTY запуск

Запись:
  ~/.claude/config/onboarding.toml — выбор lang/transport/provider/model
  ~/.claude/secrets/.env           — ключи провайдера
  ~/.claude/.onboarded             — флаг прохождения

Парсер toml — pure awk (без зависимостей). Реестры из submodule
_blocks/registries. Submodule bumped до afe0c6f с новым полем transport.

Fallback если submodule не подтянут: anthropic + sonnet.
2026-05-17 02:24:50 +08:00
Parfii-bot
35136a9840 feat(npm-publish): keigit as primary registry, npmjs reserved for future
- _ts_packages/tsconfig.base.json: sourceMap=false, declarationMap=false
  (source maps leaked absolute dev paths in published tarballs).
- All 6 @keisei/* packages: publishConfig.registry = keigit.com.
  mcp-server bumped 0.14.5 -> 0.14.6 (republished without maps).
- .github/workflows/release.yml split into two jobs:
    npm-publish-keigit: primary. Activates on KEIGIT_NPM_TOKEN +
      KEIGIT_NPM_USER secrets. Publishes via direct curl PUT
      (Forgejo requires Basic auth; npm CLI sends Bearer).
    npm-publish-npmjs: reserved for future. Activates on NPM_TOKEN
      secret. Currently no token -> job skipped gracefully.

End-to-end verified: clean dir + scope @keisei -> keigit + npm install
pulls 145 deps, no leaked paths, no .map files in any of 6 packages.
2026-05-17 00:18:44 +08:00
46 changed files with 1863 additions and 28 deletions

View file

@ -295,12 +295,116 @@ jobs:
done done
echo "✓ Release $TAG published with all assets" echo "✓ Release $TAG published with all assets"
npm-publish: # ─────────────────────────────────────────────────────────────────────
name: Publish npm packages (optional) # npm publish — две независимые job'ы.
#
# PRIMARY: keigit.com (наш приватный Forgejo). Активируется когда
# установлен secret KEIGIT_NPM_TOKEN. Forgejo требует
# Basic-auth (`Authorization: Basic base64(user:token)`),
# поэтому публикация через прямой curl PUT с manual payload —
# npm CLI не умеет Basic для Forgejo packages API.
#
# FUTURE: registry.npmjs.org. Активируется когда установлен secret
# NPM_TOKEN. Сейчас не подключено (secret не задан) — job
# gracefully скипается. Оставлен для будущего публичного
# хостинга когда захотим.
# ─────────────────────────────────────────────────────────────────────
npm-publish-keigit:
name: Publish to keigit.com (primary)
needs: release needs: release
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Graceful skip: if NPM_TOKEN secret is not configured, the first step steps:
# reports "skipped" and exits 0 — Rust-binary release above still succeeds. - name: Check KEIGIT_NPM_TOKEN presence
id: have_token
env:
KEIGIT_NPM_TOKEN: ${{ secrets.KEIGIT_NPM_TOKEN }}
run: |
if [ -n "${KEIGIT_NPM_TOKEN:-}" ]; then
echo "present=1" >> "$GITHUB_OUTPUT"
else
echo "present=0" >> "$GITHUB_OUTPUT"
echo "::notice::KEIGIT_NPM_TOKEN not set — skipping keigit publish gracefully"
fi
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
if: steps.have_token.outputs.present == '1'
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
if: steps.have_token.outputs.present == '1'
with:
node-version: '20'
- name: Install deps
if: steps.have_token.outputs.present == '1'
working-directory: _ts_packages
run: npm ci
- name: Build workspaces
if: steps.have_token.outputs.present == '1'
working-directory: _ts_packages
run: npm run build --workspaces --if-present
- name: Publish each package via curl PUT
if: steps.have_token.outputs.present == '1'
working-directory: _ts_packages
env:
KEIGIT_NPM_TOKEN: ${{ secrets.KEIGIT_NPM_TOKEN }}
KEIGIT_NPM_USER: ${{ secrets.KEIGIT_NPM_USER }}
run: |
set -euo pipefail
: "${KEIGIT_NPM_USER:?KEIGIT_NPM_USER secret required (e.g. 'Parfionovich')}"
B64_AUTH=$(printf '%s' "${KEIGIT_NPM_USER}:${KEIGIT_NPM_TOKEN}" | base64 -w0)
for pkg in packages/*/; do
[ -f "$pkg/package.json" ] || continue
pkgname=$(jq -r '.name' "$pkg/package.json")
version=$(jq -r '.version' "$pkg/package.json")
short=$(echo "$pkgname" | cut -d/ -f2)
echo "::group::publish $pkgname@$version → keigit"
(
cd "$pkg"
npm pack >/dev/null
tarball="keisei-${short}-${version}.tgz"
[ -f "$tarball" ] || { echo "::warning::tarball $tarball missing"; exit 0; }
data=$(base64 -w0 "$tarball")
shasum=$(sha1sum "$tarball" | awk '{print $1}')
integrity="sha512-$(sha512sum "$tarball" | awk '{print $1}' | xxd -r -p | base64 -w0)"
size=$(stat -c '%s' "$tarball")
jq -n \
--arg name "$pkgname" --arg version "$version" \
--arg tarball "https://keigit.com/api/packages/keisei/npm/%40keisei%2F${short}/-/${version}/${short}-${version}.tgz" \
--arg shasum "$shasum" --arg integrity "$integrity" \
--arg data "$data" --argjson length "$size" \
--arg attach "${short}-${version}.tgz" --slurpfile pkg package.json \
'{ _id: $name, name: $name, "dist-tags": {latest: $version},
versions: { ($version): ($pkg[0] + {_id: ($name + "@" + $version), dist: {tarball: $tarball, shasum: $shasum, integrity: $integrity}}) },
_attachments: ({} | .[$attach] = { content_type:"application/octet-stream", data:$data, length:$length }) }' > payload.json
http=$(curl -sS -X PUT "https://keigit.com/api/packages/keisei/npm/@keisei%2F${short}" \
-H "Authorization: Basic ${B64_AUTH}" -H "Content-Type: application/json" \
--data-binary @payload.json -o resp.txt -w "%{http_code}")
if [ "$http" = "201" ]; then
echo "$pkgname@$version → keigit OK"
elif [ "$http" = "409" ] || grep -q "already exists" resp.txt 2>/dev/null; then
echo "::warning::$pkgname@$version already published (skipping)"
else
echo "::error::$pkgname@$version → HTTP $http"
cat resp.txt
exit 1
fi
rm -f "$tarball" payload.json resp.txt
)
echo "::endgroup::"
done
npm-publish-npmjs:
name: Publish to registry.npmjs.org (future, gracefully skipped)
needs: release
runs-on: ubuntu-latest
# FUTURE: добавит публичный хостинг через npmjs параллельно keigit.
# Сейчас secret NPM_TOKEN не установлен → job просто скипается.
# Когда захотим подключить — добавить secret NPM_TOKEN с
# https://www.npmjs.com/settings/<user>/tokens, scope=Automation.
steps: steps:
- name: Check NPM_TOKEN presence - name: Check NPM_TOKEN presence
id: have_token id: have_token
@ -311,7 +415,7 @@ jobs:
echo "present=1" >> "$GITHUB_OUTPUT" echo "present=1" >> "$GITHUB_OUTPUT"
else else
echo "present=0" >> "$GITHUB_OUTPUT" echo "present=0" >> "$GITHUB_OUTPUT"
echo "::notice::NPM_TOKEN not set — skipping npm publish gracefully" echo "::notice::NPM_TOKEN not set — skipping npmjs publish gracefully (keigit publish is primary)"
fi fi
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
@ -333,7 +437,7 @@ jobs:
working-directory: _ts_packages working-directory: _ts_packages
run: npm run build --workspaces --if-present run: npm run build --workspaces --if-present
- name: Publish each package - name: Publish each package via npm CLI (override registry)
if: steps.have_token.outputs.present == '1' if: steps.have_token.outputs.present == '1'
working-directory: _ts_packages working-directory: _ts_packages
env: env:
@ -342,9 +446,10 @@ jobs:
set -euo pipefail set -euo pipefail
for pkg in packages/*/; do for pkg in packages/*/; do
if [ -f "$pkg/package.json" ]; then if [ -f "$pkg/package.json" ]; then
echo "::group::publish $pkg" echo "::group::publish $pkg → npmjs"
( cd "$pkg" && npm publish --access public ) \ # --registry overrides publishConfig.registry (keigit) for this run.
|| echo "::warning::publish failed for $pkg (continuing)" ( cd "$pkg" && npm publish --access public --registry=https://registry.npmjs.org ) \
|| echo "::warning::npmjs publish failed for $pkg (continuing)"
echo "::endgroup::" echo "::endgroup::"
fi fi
done done

149
_blocks/INDEX.md Normal file
View file

@ -0,0 +1,149 @@
# Реестр блоков KeiSeiKit
> SSoT для assembler. Все блоки доступные для `blocks = [...]` в `_manifests/<agent>.toml`.
> Авто-генерируется из `_blocks/*.md` через `bash build-index.sh`.
> Каждый файл = атомарный кубик (Constructor Pattern).
Пример:
```toml
blocks = ["baseline", "rule-pre-dev-gate", "api-anthropic"]
```
## По категориям
### API
- `api-anthropic` — API — Anthropic (Claude)
- `api-apify` — API — Apify (web scraping platform)
- `api-elevenlabs` — API — ElevenLabs (voice)
- `api-fal-ai` — API — fal.ai (image / video / 3D)
- `api-graphql` — API — GraphQL (schema-first, DataLoader, subscriptions, persisted queries)
- `api-openapi-first` — API — OpenAPI-First (3.1 as single source of truth)
- `api-rest-conventions` — API — REST Conventions (verbs, status codes, resources, idempotency, ETag)
- `api-versioning-pagination-ratelimit` — API — Versioning, Pagination, Rate Limiting
### AUTH
- `auth-authorization` — AUTH — Authorization (RBAC / ABAC / ReBAC)
- `auth-oauth2-oidc` — AUTH — OAuth2 + OIDC (Authorization Code + PKCE)
- `auth-passkeys` — AUTH — Passkeys (WebAuthn / FIDO2)
- `auth-sessions` — AUTH — Sessions & Cookies (+JWT tradeoff)
### CI
- `ci-forgejo-actions` — CI — Forgejo Actions (self-hosted, Tailscale-only admin)
- `ci-github-actions` — CI — GitHub Actions (OIDC, matrix, cache, reusable workflows)
- `ci-release-automation` — CI — Release automation (SemVer, changelog, tagging)
- `ci-security-gate` — CI — Security gate (secrets, SCA, SBOM, semgrep, licenses)
### DB
- `db-drizzle` — DB — Drizzle ORM (TypeScript) patterns
- `db-migration-hygiene` — DB — Migration hygiene (universal)
- `db-postgres` — DB — PostgreSQL (current major — 17 as of 2026-04) patterns
- `db-sqlite` — DB — SQLite (prod-suitable) patterns
- `db-sqlx` — DB — SQLx (Rust) patterns
### DEPLOY
- `deploy-aws-ec2` — DEPLOY — AWS EC2 (Instance Connect + Elastic IP)
- `deploy-cloudflare` — DEPLOY — Cloudflare (Workers / Pages / R2 / KV)
- `deploy-docker` — DEPLOY — Docker
- `deploy-hetzner-cloud` — DEPLOY — Hetzner Cloud (CX22 / CAX11 + TF + Cloud Firewall)
- `deploy-local-only` — DEPLOY — LOCAL ONLY (sensitive / pre-disclosure project)
- `deploy-modal` — DEPLOY — Modal (GPU compute)
- `deploy-vps-generic` — DEPLOY — Generic VPS (provider-agnostic cloud-init + ssh-first-contact)
### DOCS
- `docs-architecture-diagrams` — DOCS — Architecture diagrams (Mermaid)
- `docs-claude-md` — DOCS — `CLAUDE.md` (project bootstrap template)
- `docs-decisions-adr` — DOCS — `DECISIONS.md` / ADR template (MADR 4.0)
- `docs-readme-template` — DOCS — Public `README.md` scaffold
- `docs-runbook` — DOCS — Operational runbook template
### DOMAIN
- `domain-has-secrets` — DOMAIN — Secrets handling
- `domain-ml-training` — DOMAIN — ML Training
- `domain-paid-apis` — DOMAIN — Paid APIs (Anthropic / OpenAI / fal.ai / Apify / Modal / AWS / GCP / ElevenLabs)
### MODE
- `mode-devils-advocate` — MODE — Devil's Advocate
- `mode-first-principles` — MODE — First Principles
- `mode-matrix` — MODE — Agent × Cognitive-Mode Matrix
- `mode-maximalist` — MODE — Maximalist
- `mode-minimalist` — MODE — Minimalist
- `mode-skeptic` — MODE — Skeptic
### OBS
- `obs-metrics` — OBSERVABILITY — Metrics (Prometheus + OTel + RED/USE)
- `obs-structured-logs` — OBSERVABILITY — Structured logs (JSON-lines)
- `obs-traces` — OBSERVABILITY — Distributed traces (OpenTelemetry + W3C traceparent)
### PATH
- `path-user-hooks` — Path atom — user-hooks
- `path-user-memory` — Path atom — user-memory
- `path-user-rules` — Path atom — user-rules
### RULE
- `rule-double-audit` — DOUBLE AUDIT PROTOCOL (mandatory when 3+ files touched)
- `rule-error-budget` — ERROR BUDGET — 3-Level Escalation
- `rule-math-first` — MATH FIRST (mandatory for ML / physics / theory work)
- `rule-pre-dev-gate` — PRE-DEV GATE — three checks before any new code
- `rule-pure-click-contract` — Pure-Click Contract
- `rule-test-first` — TEST-FIRST
### SCRAPER
- `scraper-free-tier` — DOMAIN — Scrapers Tier 1 (free APIs + open-source)
- `scraper-paid-tier` — DOMAIN — Scrapers Tier 3 (Apify / Bright Data paid)
- `scraper-unified-output` — DOMAIN — Scraper unified output invariant
### SECURITY
- `security-audit-logging` — SECURITY — Audit Logging (auditd + journald forwarding)
- `security-firewall-ufw` — SECURITY — Firewall (ufw default-deny + rate limiting + nftables alt)
- `security-patching` — SECURITY — Patching (unattended-upgrades + needrestart + reboot window)
- `security-ssh-hardening` — SECURITY — SSH Hardening (sshd_config.d/99-kei.conf)
- `security-tls-caddy` — SECURITY — TLS via Caddy (automatic ACME, HTTP-01 / DNS-01)
### STACK
- `stack-astro` — STACK — Astro 6 (Content + Marketing + Islands)
- `stack-embedded-stm32` — STACK — Embedded Rust STM32 (embassy / cortex-m)
- `stack-fastapi-postgres` — STACK — FastAPI + async SQLAlchemy 2.0 + PostgreSQL
- `stack-flutter` — STACK — Flutter + Riverpod + Clean Architecture
- `stack-go-server` — STACK — Go server
- `stack-nextjs` — STACK — Next.js 15/16 (App Router + TS + Server Components)
- `stack-python-ml` — STACK — Python ML (PyTorch / JAX)
- `stack-react-vite` — STACK — Vite + React 19 + TypeScript (SPA)
- `stack-rust-axum` — STACK — Rust HTTP server (axum + tokio + sqlx)
- `stack-rust-cli` — STACK — Rust CLI / tooling
- `stack-sveltekit` — STACK — SvelteKit (Svelte 5 Runes + TS)
- `stack-swift-ios` — STACK — Swift iOS (UIKit / SwiftUI hybrid)
- `stack-swift-spm` — STACK — Swift SPM executable (macOS)
- `stack-tailwind` — STACK — Tailwind CSS 4 (compositional add-on)
### TEST
- `test-e2e` — TEST — End-to-end (Playwright browser automation)
- `test-fuzz` — TEST — Fuzzing (input-space exploration)
- `test-load` — TEST — Load / performance testing (baseline → profile → fix)
- `test-property` — TEST — Property-based testing (invariants + shrinking)
### Прочие (без категорийного префикса)
- `baseline` — BASELINE — inherit from Main Claude (never violate)
- `evidence-grading` — EVIDENCE GRADING
- `memory-protocol` — MEMORY PROTOCOL
- `pipeline-5phase-template` — Pipeline 5-Phase Wizard Template (shared preamble)
---
Всего блоков: 84.
Перегенерация: `bash _blocks/build-index.sh`.

72
_blocks/build-index.sh Executable file
View file

@ -0,0 +1,72 @@
#!/usr/bin/env bash
# build-index.sh — регенерация _blocks/INDEX.md из *.md.
#
# Использование:
# cd ~/Projects/KeiSeiKit-public/_blocks && bash build-index.sh
# # или из любого места:
# bash $(git rev-parse --show-toplevel)/_blocks/build-index.sh
#
# Что делает:
# 1. Сканит _blocks/*.md (исключая README.md и INDEX.md).
# 2. Группирует по префиксу (api-, auth-, ci-, db-, deploy-, ...).
# 3. Для каждого блока берёт первую H1-строку как описание.
# 4. Пишет INDEX.md с разбиением по 14 категориям + "Прочие".
#
# Безопасно перезапускать — детерминированный output.
set -euo pipefail
# Запускаемся всегда из _blocks/.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
CATEGORIES=(api auth ci db deploy docs domain mode obs path rule scraper security stack test)
OUT="INDEX.md"
TMP="${OUT}.tmp.$$"
trap 'rm -f "$TMP"' EXIT
{
printf '# Реестр блоков KeiSeiKit\n\n'
printf '> SSoT для assembler. Все блоки доступные для `blocks = [...]` в `_manifests/<agent>.toml`.\n'
printf '> Авто-генерируется из `_blocks/*.md` через `bash build-index.sh`.\n'
printf '> Каждый файл = атомарный кубик (Constructor Pattern).\n\n'
printf 'Пример:\n```toml\nblocks = ["baseline", "rule-pre-dev-gate", "api-anthropic"]\n```\n\n'
printf '## По категориям\n\n'
for cat in "${CATEGORIES[@]}"; do
upper=$(echo "$cat" | tr '[:lower:]' '[:upper:]')
files=$(ls 2>/dev/null | grep -E "^${cat}(-|\.).*\.md$" || true)
[ -z "$files" ] && continue
printf '### %s\n\n' "$upper"
while IFS= read -r f; do
[ -z "$f" ] && continue
name="${f%.md}"
desc=$(awk '/^# / { sub(/^# /, ""); print; exit }' "$f" 2>/dev/null || true)
[ -z "$desc" ] && desc="(no title)"
printf -- '- `%s` — %s\n' "$name" "$desc"
done <<< "$files"
printf '\n'
done
printf '### Прочие (без категорийного префикса)\n\n'
while IFS= read -r f; do
name="${f%.md}"
case "$name" in
api-*|auth-*|ci-*|db-*|deploy-*|docs-*|domain-*|mode-*|obs-*|path-*|rule-*|scraper-*|security-*|stack-*|test-*|README|INDEX) continue ;;
esac
desc=$(awk '/^# / { sub(/^# /, ""); print; exit }' "$f" 2>/dev/null || true)
[ -z "$desc" ] && desc="(no title)"
printf -- '- `%s` — %s\n' "$name" "$desc"
done < <(ls *.md)
total=$(ls *.md | grep -vE '^(README|INDEX)\.md$' | wc -l | tr -d ' ')
printf '\n---\n\nВсего блоков: %d.\n' "$total"
printf 'Перегенерация: `bash _blocks/build-index.sh`.\n'
} > "$TMP"
mv "$TMP" "$OUT"
trap - EXIT
echo "$OUT regenerated"
wc -l "$OUT"

@ -1 +1 @@
Subproject commit 7aaa6a79b00271d9c08ac4f5c1f0e2d523a49da0 Subproject commit afe0c6f1183deaf4d3947bb6a4bf279a6bf9418e

View file

@ -41,7 +41,7 @@ chrono = { workspace = true }
# provision-crypto: x25519 ECDH + HKDF-SHA256 + XChaCha20-Poly1305 # provision-crypto: x25519 ECDH + HKDF-SHA256 + XChaCha20-Poly1305
# Mirrors marketplace/src/lib/crypto-box.ts so VPS can decrypt the # Mirrors marketplace/src/lib/crypto-box.ts so VPS can decrypt the
# bot-token blob emitted by the browser. # bot-token blob emitted by the browser.
x25519-dalek = { version = "2", features = ["static_secrets"] } x25519-dalek = { version = "2", features = ["static_secrets", "zeroize"] }
chacha20poly1305 = { version = "0.10", features = ["alloc"] } chacha20poly1305 = { version = "0.10", features = ["alloc"] }
hkdf = "0.12" hkdf = "0.12"
sha2 = "0.10" sha2 = "0.10"

View file

@ -59,6 +59,17 @@ fn b64decode(s: &str) -> Result<Vec<u8>> {
.map_err(|e| anyhow!("base64 decode: {e}")) .map_err(|e| anyhow!("base64 decode: {e}"))
} }
/// Парсит PKCS#8 v1 PEM с приватником X25519 (RFC 8410 §7).
///
/// Ожидаемый формат — ровно 48 байт DER, последние 32 — raw priv.
/// Проверки до взятия хвоста:
/// - длина DER ровно 48 байт
/// - OID 1.3.101.110 (X25519) по смещению 9..12: 0x2b 0x65 0x6e
///
/// Без OID-проверки RSA/EC/Ed25519 ключ молча даст 32 неправильных байта.
const X25519_OID: [u8; 3] = [0x2b, 0x65, 0x6e]; // RFC 8410 §3
const X25519_PKCS8_DER_LEN: usize = 48;
fn parse_x25519_pkcs8_pem(pem: &str) -> Result<[u8; 32]> { fn parse_x25519_pkcs8_pem(pem: &str) -> Result<[u8; 32]> {
let dash_prefix = "-".repeat(5); let dash_prefix = "-".repeat(5);
let body: String = pem let body: String = pem
@ -69,8 +80,18 @@ fn parse_x25519_pkcs8_pem(pem: &str) -> Result<[u8; 32]> {
let der = STANDARD let der = STANDARD
.decode(body.trim()) .decode(body.trim())
.context("PEM body is not valid base64")?; .context("PEM body is not valid base64")?;
if der.len() < 32 { if der.len() != X25519_PKCS8_DER_LEN {
bail!("PKCS#8 DER too short: {} bytes", der.len()); bail!(
"PKCS#8 DER must be {} bytes for X25519, got {}",
X25519_PKCS8_DER_LEN,
der.len()
);
}
if der[9..12] != X25519_OID {
bail!(
"PKCS#8 OID does not match X25519 (1.3.101.110); got {:02x?}",
&der[9..12]
);
} }
let mut out = [0u8; 32]; let mut out = [0u8; 32];
out.copy_from_slice(&der[der.len() - 32..]); out.copy_from_slice(&der[der.len() - 32..]);
@ -329,6 +350,37 @@ mod tests {
assert_eq!(b64decode(urlsafe).unwrap(), b"Hello world"); assert_eq!(b64decode(urlsafe).unwrap(), b"Hello world");
} }
#[test]
fn parse_rejects_wrong_length_der() {
// ровно 32 байта — слишком короткий для PKCS#8 v1 wrapper
let bad_pem = format!(
"{}\n{}\n{}\n",
pem_begin(),
STANDARD.encode([0u8; 32]),
pem_end()
);
let err = parse_x25519_pkcs8_pem(&bad_pem).err().unwrap();
assert!(err.to_string().contains("48 bytes"));
}
#[test]
fn parse_rejects_wrong_oid() {
// 48 байт правильной длины, но OID не X25519 (например Ed25519: 0x2b 0x65 0x70)
let mut der = vec![
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22,
0x04, 0x20,
];
der.extend_from_slice(&[0u8; 32]);
let bad_pem = format!(
"{}\n{}\n{}\n",
pem_begin(),
STANDARD.encode(&der),
pem_end()
);
let err = parse_x25519_pkcs8_pem(&bad_pem).err().unwrap();
assert!(err.to_string().contains("X25519"));
}
fn tempdir_unique() -> std::path::PathBuf { fn tempdir_unique() -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0); static COUNTER: AtomicU64 = AtomicU64::new(0);

View file

@ -6,11 +6,22 @@
//! Types in `registry_types.rs` (Constructor Pattern: types separate from loader). //! Types in `registry_types.rs` (Constructor Pattern: types separate from loader).
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Deserialize;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
pub use crate::registry_types::{Model, Profile, Provider}; pub use crate::registry_types::{Model, Profile, Provider};
use crate::registry_types::{ModelsFile, ProfilesFile, ProvidersFile}; use crate::registry_types::{ModelsFile, ProfilesFile, ProvidersFile};
/// User-tier override: что пользователь выбрал в onboarding мастере.
/// Парсится из `~/.claude/config/user-model-override.toml`.
#[derive(Debug, Clone, Deserialize)]
pub struct UserModelOverride {
pub provider: String,
pub model: String,
#[serde(default)]
pub transport: Option<String>,
}
// Embedded compile-time copies. Cargo tracks these as implicit dependencies: // Embedded compile-time copies. Cargo tracks these as implicit dependencies:
// if the TOML changes, the crate is recompiled automatically. // if the TOML changes, the crate is recompiled automatically.
const EMBEDDED_PROVIDERS: &str = const EMBEDDED_PROVIDERS: &str =
@ -65,6 +76,25 @@ impl Registry {
}) })
} }
/// Загружает user-tier override из `~/.claude/config/user-model-override.toml`.
/// Файл пишется установщиком (install/lib-onboarding.sh::onboarding_write_config)
/// при первичной настройке. Содержит выбор юзера: provider/model/transport.
///
/// Priority в router: `--pinned` flag > этот файл > agent-profiles.toml::default_model_ref.
/// Без него выбор провайдера в onboarding декоративен (HIGH аудит-1, 2026-05-17).
pub fn load_user_override() -> Option<UserModelOverride> {
let home = std::env::var("HOME").ok()?;
if home.is_empty() {
return None;
}
let path = PathBuf::from(format!("{home}/.claude/config/user-model-override.toml"));
if !path.exists() {
return None;
}
let raw = std::fs::read_to_string(&path).ok()?;
toml::from_str::<UserModelOverride>(&raw).ok()
}
pub fn provider_by_id(&self, id: &str) -> Option<&Provider> { pub fn provider_by_id(&self, id: &str) -> Option<&Provider> {
self.providers.iter().find(|p| p.id == id) self.providers.iter().find(|p| p.id == id)
} }
@ -137,7 +167,38 @@ mod tests {
fn provider_by_id_anthropic() { fn provider_by_id_anthropic() {
let r = reg(); let r = reg();
let p = r.provider_by_id("anthropic").expect("anthropic missing"); let p = r.provider_by_id("anthropic").expect("anthropic missing");
assert_eq!(p.display_name, "Anthropic"); // После RULE 0.26 transport-расширения display_name стало
// "Anthropic (Direct API)" чтобы отличать от "anthropic-bedrock".
assert!(
p.display_name.starts_with("Anthropic"),
"got: {}", p.display_name
);
}
#[test]
fn user_override_parses_minimal_toml() {
// Round-trip: TOML → UserModelOverride.
let toml_src = r#"
provider = "ollama-local"
model = "llama-3.3-70b"
transport = "local"
"#;
let ov: UserModelOverride = toml::from_str(toml_src).expect("parse failed");
assert_eq!(ov.provider, "ollama-local");
assert_eq!(ov.model, "llama-3.3-70b");
assert_eq!(ov.transport.as_deref(), Some("local"));
}
#[test]
fn user_override_transport_optional() {
// transport — optional поле.
let toml_src = r#"
provider = "openai"
model = "gpt-5"
"#;
let ov: UserModelOverride = toml::from_str(toml_src).expect("parse failed");
assert_eq!(ov.provider, "openai");
assert!(ov.transport.is_none());
} }
#[test] #[test]

View file

@ -30,5 +30,9 @@
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"author": "Denis Parfionovich <parfionovich@keilab.io>" "author": "Denis Parfionovich <parfionovich@keilab.io>",
"publishConfig": {
"registry": "https://keigit.com/api/packages/keisei/npm/",
"access": "public"
}
} }

View file

@ -29,5 +29,9 @@
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"author": "Denis Parfionovich <parfionovich@keilab.io>" "author": "Denis Parfionovich <parfionovich@keilab.io>",
"publishConfig": {
"registry": "https://keigit.com/api/packages/keisei/npm/",
"access": "public"
}
} }

View file

@ -0,0 +1,7 @@
# Source maps leak absolute paths of dev machine.
# Tested 2026-05-15: dist/*.js.map content includes "/Users/<dev>/Projects/..." strings.
**/*.map
**/*.tsbuildinfo
src/
test/
tsconfig*.json

View file

@ -1,6 +1,6 @@
{ {
"name": "@keisei/mcp-server", "name": "@keisei/mcp-server",
"version": "0.14.5", "version": "0.14.6",
"description": "MCP server exposing KeiSeiKit Rust primitives as Model Context Protocol tools — published to keigit.com (Forgejo npm registry, public DNS)", "description": "MCP server exposing KeiSeiKit Rust primitives as Model Context Protocol tools — published to keigit.com (Forgejo npm registry, public DNS)",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View file

@ -3,7 +3,9 @@
"compilerOptions": { "compilerOptions": {
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./src",
"types": ["node"] "types": ["node"],
"sourceMap": false,
"declarationMap": false
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["dist", "node_modules", "test/**/*"] "exclude": ["dist", "node_modules", "test/**/*"]

View file

@ -29,5 +29,9 @@
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"author": "Denis Parfionovich <parfionovich@keilab.io>" "author": "Denis Parfionovich <parfionovich@keilab.io>",
"publishConfig": {
"registry": "https://keigit.com/api/packages/keisei/npm/",
"access": "public"
}
} }

View file

@ -30,5 +30,9 @@
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"author": "Denis Parfionovich <parfionovich@keilab.io>" "author": "Denis Parfionovich <parfionovich@keilab.io>",
"publishConfig": {
"registry": "https://keigit.com/api/packages/keisei/npm/",
"access": "public"
}
} }

View file

@ -31,5 +31,9 @@
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"author": "Denis Parfionovich <parfionovich@keilab.io>" "author": "Denis Parfionovich <parfionovich@keilab.io>",
"publishConfig": {
"registry": "https://keigit.com/api/packages/keisei/npm/",
"access": "public"
}
} }

View file

@ -14,8 +14,8 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"skipLibCheck": true, "skipLibCheck": true,
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": false,
"sourceMap": true, "sourceMap": false,
"composite": true, "composite": true,
"incremental": true "incremental": true
} }

View file

@ -41,6 +41,14 @@ source "$LIB_DIR/lib-profile.sh"
source "$LIB_DIR/lib-args.sh" source "$LIB_DIR/lib-args.sh"
# shellcheck source=install/lib-menu.sh # shellcheck source=install/lib-menu.sh
source "$LIB_DIR/lib-menu.sh" source "$LIB_DIR/lib-menu.sh"
# shellcheck source=install/lib-i18n.sh
source "$LIB_DIR/lib-i18n.sh"
# Загружаем английский словарь по умолчанию — welcome banner идёт до выбора языка.
i18n_load_default
# shellcheck source=install/lib-preflight.sh
source "$LIB_DIR/lib-preflight.sh"
# shellcheck source=install/lib-onboarding.sh
source "$LIB_DIR/lib-onboarding.sh"
# shellcheck source=install/lib-plan.sh # shellcheck source=install/lib-plan.sh
source "$LIB_DIR/lib-plan.sh" source "$LIB_DIR/lib-plan.sh"
# shellcheck source=install/lib-prereqs.sh # shellcheck source=install/lib-prereqs.sh
@ -140,6 +148,15 @@ case "$PROFILE" in
esac esac
say "profile: $PROFILE" say "profile: $PROFILE"
# --- welcome banner + onboarding wizard ----------------------------------
# Banner всегда EN — пользователь ещё не выбрал язык.
# Wizard: TTY + нет ~/.claude/.onboarded + не задан KEISEI_SKIP_ONBOARD.
# Skip: KEISEI_SKIP_ONBOARD=1 ./install.sh
if onboarding_should_run; then
i18n_print_welcome
fi
onboarding_run
# --- prerequisites ------------------------------------------------------- # --- prerequisites -------------------------------------------------------
check_prereqs check_prereqs

35
install/i18n/ar.sh Normal file
View file

@ -0,0 +1,35 @@
# shellcheck shell=bash
# i18n/ar.sh — العربية.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
STR_ONBOARDING_INTRO="معالج الإعداد الأولي (5 خطوات)"
STR_PICK_LANGUAGE="اختر لغة الواجهة:"
STR_PICK_TRANSPORT="اختر طريقة الاتصال:"
STR_PICK_PROVIDER="اختر المزود ضمن المجموعة"
STR_PICK_MODEL="النموذج الافتراضي:"
STR_TR_DIRECT_API="واجهة برمجية مباشرة للمزود (مفتاح)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/دور)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+مفتاح)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="محلي (Ollama/MLX/LMStudio)"
STR_TR_PROXY="وكيل (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="اشتراك OAuth (ChatGPT)"
STR_AUTH_INTRO="المصادقة لـ"
STR_AUTH_PROMPT="أدخل القيم (Enter — اتركها فارغة، املأها لاحقًا)."
STR_AUTH_CURRENT_HINT="(الحالي: <مخفي>)"
STR_DONE_TITLE="اكتمل الإعداد الأولي"
STR_DONE_CONFIG="التكوين:"
STR_DONE_SECRETS="الأسرار:"
STR_MENU_TITLE="مثبّت KeiSeiKit"
STR_MENU_SUBSTRATE="القاعدة الأساسية (دائمًا مثبتة):"
STR_MENU_PROFILE_PROMPT="اختر ملف التثبيت:"
STR_MENU_CONFIRM="تأكيد الاختيار؟"
STR_PREFLIGHT_FAILED="فشل Preflight — قد لا يعمل المزود."
STR_PREFLIGHT_CONTINUE="هل تريد المتابعة على أي حال؟ [y/N]"

35
install/i18n/de.sh Normal file
View file

@ -0,0 +1,35 @@
# shellcheck shell=bash
# i18n/de.sh — Deutsch.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
STR_ONBOARDING_INTRO="Einrichtungsassistent (5 Schritte)"
STR_PICK_LANGUAGE="Oberflächensprache wählen:"
STR_PICK_TRANSPORT="Verbindungsart wählen:"
STR_PICK_PROVIDER="Anbieter in der Gruppe wählen"
STR_PICK_MODEL="Standardmodell:"
STR_TR_DIRECT_API="Direkte API des Anbieters (Schlüssel)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/Rolle)"
STR_TR_AZURE_OPENAI="Azure OpenAI (Deployment+Schlüssel)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Lokal (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Proxy (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="OAuth-Abo (ChatGPT)"
STR_AUTH_INTRO="Authentifizierung für"
STR_AUTH_PROMPT="Werte eingeben (Enter — leer lassen, später ausfüllen)."
STR_AUTH_CURRENT_HINT="(aktuell: <ausgeblendet>)"
STR_DONE_TITLE="Einrichtung abgeschlossen"
STR_DONE_CONFIG="Konfig:"
STR_DONE_SECRETS="Geheimnisse:"
STR_MENU_TITLE="KeiSeiKit Installer"
STR_MENU_SUBSTRATE="Substrat-Basis (immer installiert):"
STR_MENU_PROFILE_PROMPT="Installationsprofil wählen:"
STR_MENU_CONFIRM="Auswahl bestätigen?"
STR_PREFLIGHT_FAILED="Preflight fehlgeschlagen — Anbieter funktioniert evtl. nicht."
STR_PREFLIGHT_CONTINUE="Trotzdem fortfahren? [j/N]"

42
install/i18n/en.sh Normal file
View file

@ -0,0 +1,42 @@
# shellcheck shell=bash
# i18n/en.sh — English strings. Default before user picks language.
# Welcome banner (always EN, shown before language picker).
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
# Onboarding wizard steps
STR_ONBOARDING_INTRO="Onboarding wizard (5 steps)"
STR_PICK_LANGUAGE="Choose interface language:"
STR_PICK_TRANSPORT="Choose connection transport:"
STR_PICK_PROVIDER="Choose provider within"
STR_PICK_MODEL="Default model:"
# Transport descriptions
STR_TR_DIRECT_API="Direct provider API (key)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/role)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+key)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Local (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Proxy (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="OAuth subscription (ChatGPT)"
# Auth collection
STR_AUTH_INTRO="Auth for"
STR_AUTH_PROMPT="Enter values (Enter — leave empty, fill later)."
STR_AUTH_CURRENT_HINT="(current: <hidden>)"
# Completion
STR_DONE_TITLE="Onboarding complete"
STR_DONE_CONFIG="config:"
STR_DONE_SECRETS="secrets:"
# Profile menu (lib-menu.sh strings)
STR_MENU_TITLE="KeiSeiKit Installer"
STR_MENU_SUBSTRATE="Substrate baseline (always installed):"
STR_MENU_PROFILE_PROMPT="Choose install profile:"
STR_MENU_CONFIRM="Confirm selection?"
# Preflight warnings
STR_PREFLIGHT_FAILED="Preflight failed — provider may not work."
STR_PREFLIGHT_CONTINUE="Continue anyway? [y/N]"

35
install/i18n/es.sh Normal file
View file

@ -0,0 +1,35 @@
# shellcheck shell=bash
# i18n/es.sh — Español.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
STR_ONBOARDING_INTRO="Asistente de configuración inicial (5 pasos)"
STR_PICK_LANGUAGE="Elige el idioma de la interfaz:"
STR_PICK_TRANSPORT="Elige el método de conexión:"
STR_PICK_PROVIDER="Elige el proveedor dentro de"
STR_PICK_MODEL="Modelo por defecto:"
STR_TR_DIRECT_API="API directa del proveedor (clave)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/rol)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+clave)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Local (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Proxy (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="Suscripción OAuth (ChatGPT)"
STR_AUTH_INTRO="Autenticación para"
STR_AUTH_PROMPT="Introduce los valores (Enter — dejar vacío, completar luego)."
STR_AUTH_CURRENT_HINT="(actual: <oculto>)"
STR_DONE_TITLE="Configuración inicial completada"
STR_DONE_CONFIG="config:"
STR_DONE_SECRETS="secretos:"
STR_MENU_TITLE="Instalador de KeiSeiKit"
STR_MENU_SUBSTRATE="Base del sustrato (siempre instalada):"
STR_MENU_PROFILE_PROMPT="Elige el perfil de instalación:"
STR_MENU_CONFIRM="¿Confirmar selección?"
STR_PREFLIGHT_FAILED="Preflight falló — el proveedor puede no funcionar."
STR_PREFLIGHT_CONTINUE="¿Continuar de todos modos? [s/N]"

35
install/i18n/fr.sh Normal file
View file

@ -0,0 +1,35 @@
# shellcheck shell=bash
# i18n/fr.sh — Français.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
STR_ONBOARDING_INTRO="Assistant de configuration initiale (5 étapes)"
STR_PICK_LANGUAGE="Choisir la langue de l'interface :"
STR_PICK_TRANSPORT="Choisir le mode de connexion :"
STR_PICK_PROVIDER="Choisir le fournisseur dans le groupe"
STR_PICK_MODEL="Modèle par défaut :"
STR_TR_DIRECT_API="API directe du fournisseur (clé)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/rôle)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+clé)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Local (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Proxy (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="Abonnement OAuth (ChatGPT)"
STR_AUTH_INTRO="Authentification pour"
STR_AUTH_PROMPT="Saisir les valeurs (Entrée — laisser vide, remplir plus tard)."
STR_AUTH_CURRENT_HINT="(actuel : <masqué>)"
STR_DONE_TITLE="Configuration initiale terminée"
STR_DONE_CONFIG="config :"
STR_DONE_SECRETS="secrets :"
STR_MENU_TITLE="Installateur KeiSeiKit"
STR_MENU_SUBSTRATE="Base du substrat (toujours installée) :"
STR_MENU_PROFILE_PROMPT="Choisir le profil d'installation :"
STR_MENU_CONFIRM="Confirmer la sélection ?"
STR_PREFLIGHT_FAILED="Preflight échoué — le fournisseur peut ne pas fonctionner."
STR_PREFLIGHT_CONTINUE="Continuer quand même ? [o/N]"

35
install/i18n/hi.sh Normal file
View file

@ -0,0 +1,35 @@
# shellcheck shell=bash
# i18n/hi.sh — हिन्दी.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
STR_ONBOARDING_INTRO="प्रारंभिक सेटअप विज़ार्ड (5 चरण)"
STR_PICK_LANGUAGE="इंटरफ़ेस भाषा चुनें:"
STR_PICK_TRANSPORT="कनेक्शन विधि चुनें:"
STR_PICK_PROVIDER="समूह में प्रदाता चुनें"
STR_PICK_MODEL="डिफ़ॉल्ट मॉडल:"
STR_TR_DIRECT_API="प्रदाता डायरेक्ट API (कुंजी)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/रोल)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+कुंजी)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="लोकल (Ollama/MLX/LMStudio)"
STR_TR_PROXY="प्रॉक्सी (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="OAuth सब्सक्रिप्शन (ChatGPT)"
STR_AUTH_INTRO="के लिए प्रमाणीकरण"
STR_AUTH_PROMPT="मान दर्ज करें (Enter — खाली छोड़ें, बाद में भरें)।"
STR_AUTH_CURRENT_HINT="(वर्तमान: <छिपा हुआ>)"
STR_DONE_TITLE="प्रारंभिक सेटअप पूरा हुआ"
STR_DONE_CONFIG="कॉन्फ़िग:"
STR_DONE_SECRETS="सीक्रेट्स:"
STR_MENU_TITLE="KeiSeiKit इंस्टॉलर"
STR_MENU_SUBSTRATE="सब्सट्रेट बेस (हमेशा स्थापित):"
STR_MENU_PROFILE_PROMPT="इंस्टॉल प्रोफ़ाइल चुनें:"
STR_MENU_CONFIRM="चयन की पुष्टि करें?"
STR_PREFLIGHT_FAILED="Preflight विफल — प्रदाता काम नहीं कर सकता।"
STR_PREFLIGHT_CONTINUE="फिर भी जारी रखें? [y/N]"

35
install/i18n/id.sh Normal file
View file

@ -0,0 +1,35 @@
# shellcheck shell=bash
# i18n/id.sh — Bahasa Indonesia.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
STR_ONBOARDING_INTRO="Wizard pengaturan awal (5 langkah)"
STR_PICK_LANGUAGE="Pilih bahasa antarmuka:"
STR_PICK_TRANSPORT="Pilih metode koneksi:"
STR_PICK_PROVIDER="Pilih penyedia dalam grup"
STR_PICK_MODEL="Model default:"
STR_TR_DIRECT_API="API langsung penyedia (kunci)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/role)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+kunci)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Lokal (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Proxy (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="Langganan OAuth (ChatGPT)"
STR_AUTH_INTRO="Autentikasi untuk"
STR_AUTH_PROMPT="Masukkan nilai (Enter — kosongkan, isi nanti)."
STR_AUTH_CURRENT_HINT="(saat ini: <tersembunyi>)"
STR_DONE_TITLE="Pengaturan awal selesai"
STR_DONE_CONFIG="konfig:"
STR_DONE_SECRETS="rahasia:"
STR_MENU_TITLE="Installer KeiSeiKit"
STR_MENU_SUBSTRATE="Basis substrat (selalu terpasang):"
STR_MENU_PROFILE_PROMPT="Pilih profil instalasi:"
STR_MENU_CONFIRM="Konfirmasi pilihan?"
STR_PREFLIGHT_FAILED="Preflight gagal — penyedia mungkin tidak berfungsi."
STR_PREFLIGHT_CONTINUE="Lanjutkan saja? [y/N]"

35
install/i18n/it.sh Normal file
View file

@ -0,0 +1,35 @@
# shellcheck shell=bash
# i18n/it.sh — Italiano.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
STR_ONBOARDING_INTRO="Procedura guidata di configurazione iniziale (5 passi)"
STR_PICK_LANGUAGE="Scegli la lingua dell'interfaccia:"
STR_PICK_TRANSPORT="Scegli il metodo di connessione:"
STR_PICK_PROVIDER="Scegli il provider nel gruppo"
STR_PICK_MODEL="Modello predefinito:"
STR_TR_DIRECT_API="API diretta del provider (chiave)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/ruolo)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+chiave)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Locale (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Proxy (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="Abbonamento OAuth (ChatGPT)"
STR_AUTH_INTRO="Autenticazione per"
STR_AUTH_PROMPT="Inserisci i valori (Invio — lascia vuoto, compila dopo)."
STR_AUTH_CURRENT_HINT="(attuale: <nascosto>)"
STR_DONE_TITLE="Configurazione iniziale completata"
STR_DONE_CONFIG="config:"
STR_DONE_SECRETS="segreti:"
STR_MENU_TITLE="Installatore KeiSeiKit"
STR_MENU_SUBSTRATE="Base del substrato (sempre installata):"
STR_MENU_PROFILE_PROMPT="Scegli il profilo di installazione:"
STR_MENU_CONFIRM="Confermare la selezione?"
STR_PREFLIGHT_FAILED="Preflight fallito — il provider potrebbe non funzionare."
STR_PREFLIGHT_CONTINUE="Continuare comunque? [s/N]"

35
install/i18n/ja.sh Normal file
View file

@ -0,0 +1,35 @@
# shellcheck shell=bash
# i18n/ja.sh — 日本語.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
STR_ONBOARDING_INTRO="初期設定ウィザード (5 ステップ)"
STR_PICK_LANGUAGE="インターフェース言語を選択:"
STR_PICK_TRANSPORT="接続方法を選択:"
STR_PICK_PROVIDER="グループ内のプロバイダを選択"
STR_PICK_MODEL="デフォルトモデル:"
STR_TR_DIRECT_API="プロバイダ直接 API (キー)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/ロール)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+キー)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="ローカル (Ollama/MLX/LMStudio)"
STR_TR_PROXY="プロキシ (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="OAuth サブスクリプション (ChatGPT)"
STR_AUTH_INTRO="認証 —"
STR_AUTH_PROMPT="値を入力 (Enter — 空のまま、後で記入)。"
STR_AUTH_CURRENT_HINT="(現在: <非表示>)"
STR_DONE_TITLE="初期設定が完了しました"
STR_DONE_CONFIG="設定:"
STR_DONE_SECRETS="シークレット:"
STR_MENU_TITLE="KeiSeiKit インストーラ"
STR_MENU_SUBSTRATE="サブストレートのベース (常にインストール):"
STR_MENU_PROFILE_PROMPT="インストールプロファイルを選択:"
STR_MENU_CONFIRM="選択を確認しますか?"
STR_PREFLIGHT_FAILED="Preflight 失敗 — プロバイダが動作しない可能性があります。"
STR_PREFLIGHT_CONTINUE="それでも続行しますか? [y/N]"

35
install/i18n/ko.sh Normal file
View file

@ -0,0 +1,35 @@
# shellcheck shell=bash
# i18n/ko.sh — 한국어.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
STR_ONBOARDING_INTRO="초기 설정 마법사 (5 단계)"
STR_PICK_LANGUAGE="인터페이스 언어 선택:"
STR_PICK_TRANSPORT="연결 방법 선택:"
STR_PICK_PROVIDER="그룹 내 제공자 선택"
STR_PICK_MODEL="기본 모델:"
STR_TR_DIRECT_API="제공자 직접 API (키)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/역할)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+키)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="로컬 (Ollama/MLX/LMStudio)"
STR_TR_PROXY="프록시 (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="OAuth 구독 (ChatGPT)"
STR_AUTH_INTRO="인증 —"
STR_AUTH_PROMPT="값을 입력하세요 (Enter — 비워두고 나중에 작성)."
STR_AUTH_CURRENT_HINT="(현재: <숨김>)"
STR_DONE_TITLE="초기 설정 완료"
STR_DONE_CONFIG="설정:"
STR_DONE_SECRETS="시크릿:"
STR_MENU_TITLE="KeiSeiKit 설치 프로그램"
STR_MENU_SUBSTRATE="기본 서브스트레이트 (항상 설치):"
STR_MENU_PROFILE_PROMPT="설치 프로필 선택:"
STR_MENU_CONFIRM="선택을 확인하시겠습니까?"
STR_PREFLIGHT_FAILED="Preflight 실패 — 제공자가 작동하지 않을 수 있습니다."
STR_PREFLIGHT_CONTINUE="계속 진행하시겠습니까? [y/N]"

35
install/i18n/pt.sh Normal file
View file

@ -0,0 +1,35 @@
# shellcheck shell=bash
# i18n/pt.sh — Português.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
STR_ONBOARDING_INTRO="Assistente de configuração inicial (5 passos)"
STR_PICK_LANGUAGE="Escolha o idioma da interface:"
STR_PICK_TRANSPORT="Escolha o método de conexão:"
STR_PICK_PROVIDER="Escolha o provedor dentro do grupo"
STR_PICK_MODEL="Modelo padrão:"
STR_TR_DIRECT_API="API direta do provedor (chave)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/função)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+chave)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Local (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Proxy (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="Assinatura OAuth (ChatGPT)"
STR_AUTH_INTRO="Autenticação para"
STR_AUTH_PROMPT="Insira valores (Enter — deixar vazio, preencher depois)."
STR_AUTH_CURRENT_HINT="(atual: <oculto>)"
STR_DONE_TITLE="Configuração inicial concluída"
STR_DONE_CONFIG="config:"
STR_DONE_SECRETS="segredos:"
STR_MENU_TITLE="Instalador KeiSeiKit"
STR_MENU_SUBSTRATE="Base do substrato (sempre instalada):"
STR_MENU_PROFILE_PROMPT="Escolha o perfil de instalação:"
STR_MENU_CONFIRM="Confirmar seleção?"
STR_PREFLIGHT_FAILED="Preflight falhou — o provedor pode não funcionar."
STR_PREFLIGHT_CONTINUE="Continuar mesmo assim? [s/N]"

42
install/i18n/ru.sh Normal file
View file

@ -0,0 +1,42 @@
# shellcheck shell=bash
# i18n/ru.sh — русские строки. Source'ится после выбора языка.
# Welcome-баннер всегда EN — на момент его показа выбор ещё не сделан.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
# Шаги мастера
STR_ONBOARDING_INTRO="Мастер первичной настройки (5 шагов)"
STR_PICK_LANGUAGE="Выберите язык интерфейса:"
STR_PICK_TRANSPORT="Выберите способ подключения:"
STR_PICK_PROVIDER="Выберите провайдера в группе"
STR_PICK_MODEL="Модель по умолчанию:"
# Описание транспортов
STR_TR_DIRECT_API="Прямой API провайдера (ключ)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/role)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+ключ)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Локально (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Прокси (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="OAuth-подписка (ChatGPT)"
# Сбор ключей
STR_AUTH_INTRO="Аутентификация для"
STR_AUTH_PROMPT="Введите значения (Enter — оставить пустым, заполните позже)."
STR_AUTH_CURRENT_HINT="(текущее: <скрыто>)"
# Завершение
STR_DONE_TITLE="Первичная настройка завершена"
STR_DONE_CONFIG="конфиг:"
STR_DONE_SECRETS="секреты:"
# Меню профилей (lib-menu.sh)
STR_MENU_TITLE="Установщик KeiSeiKit"
STR_MENU_SUBSTRATE="Базовая часть (ставится всегда):"
STR_MENU_PROFILE_PROMPT="Выберите профиль установки:"
STR_MENU_CONFIRM="Подтвердить выбор?"
# Preflight-предупреждения
STR_PREFLIGHT_FAILED="Preflight упал — провайдер может не работать."
STR_PREFLIGHT_CONTINUE="Продолжить всё равно? [y/N]"

35
install/i18n/tr.sh Normal file
View file

@ -0,0 +1,35 @@
# shellcheck shell=bash
# i18n/tr.sh — Türkçe.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
STR_ONBOARDING_INTRO="İlk kurulum sihirbazı (5 adım)"
STR_PICK_LANGUAGE="Arayüz dilini seçin:"
STR_PICK_TRANSPORT="Bağlantı yöntemini seçin:"
STR_PICK_PROVIDER="Gruptaki sağlayıcıyı seçin"
STR_PICK_MODEL="Varsayılan model:"
STR_TR_DIRECT_API="Sağlayıcının doğrudan API'si (anahtar)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/rol)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+anahtar)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Yerel (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Proxy (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="OAuth aboneliği (ChatGPT)"
STR_AUTH_INTRO="Kimlik doğrulama —"
STR_AUTH_PROMPT="Değerleri girin (Enter — boş bırak, sonra doldur)."
STR_AUTH_CURRENT_HINT="(geçerli: <gizli>)"
STR_DONE_TITLE="İlk kurulum tamamlandı"
STR_DONE_CONFIG="yapılandırma:"
STR_DONE_SECRETS="gizli anahtarlar:"
STR_MENU_TITLE="KeiSeiKit Kurulum Aracı"
STR_MENU_SUBSTRATE="Substrate tabanı (her zaman kurulu):"
STR_MENU_PROFILE_PROMPT="Kurulum profilini seçin:"
STR_MENU_CONFIRM="Seçimi onayla?"
STR_PREFLIGHT_FAILED="Preflight başarısız — sağlayıcı çalışmayabilir."
STR_PREFLIGHT_CONTINUE="Yine de devam edilsin mi? [e/H]"

35
install/i18n/uk.sh Normal file
View file

@ -0,0 +1,35 @@
# shellcheck shell=bash
# i18n/uk.sh — Українська.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
STR_ONBOARDING_INTRO="Майстер первинного налаштування (5 кроків)"
STR_PICK_LANGUAGE="Виберіть мову інтерфейсу:"
STR_PICK_TRANSPORT="Виберіть спосіб підключення:"
STR_PICK_PROVIDER="Виберіть провайдера у групі"
STR_PICK_MODEL="Модель за замовчуванням:"
STR_TR_DIRECT_API="Прямий API провайдера (ключ)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/роль)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+ключ)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Локально (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Проксі (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="OAuth-підписка (ChatGPT)"
STR_AUTH_INTRO="Автентифікація для"
STR_AUTH_PROMPT="Введіть значення (Enter — залишити порожнім, заповнити пізніше)."
STR_AUTH_CURRENT_HINT="(поточне: <приховано>)"
STR_DONE_TITLE="Первинне налаштування завершено"
STR_DONE_CONFIG="конфіг:"
STR_DONE_SECRETS="секрети:"
STR_MENU_TITLE="Інсталятор KeiSeiKit"
STR_MENU_SUBSTRATE="База підкладки (завжди встановлюється):"
STR_MENU_PROFILE_PROMPT="Виберіть профіль встановлення:"
STR_MENU_CONFIRM="Підтвердити вибір?"
STR_PREFLIGHT_FAILED="Preflight упав — провайдер може не працювати."
STR_PREFLIGHT_CONTINUE="Продовжити все одно? [y/N]"

35
install/i18n/vi.sh Normal file
View file

@ -0,0 +1,35 @@
# shellcheck shell=bash
# i18n/vi.sh — Tiếng Việt.
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
STR_ONBOARDING_INTRO="Trình hướng dẫn thiết lập ban đầu (5 bước)"
STR_PICK_LANGUAGE="Chọn ngôn ngữ giao diện:"
STR_PICK_TRANSPORT="Chọn phương thức kết nối:"
STR_PICK_PROVIDER="Chọn nhà cung cấp trong nhóm"
STR_PICK_MODEL="Mô hình mặc định:"
STR_TR_DIRECT_API="API trực tiếp của nhà cung cấp (khóa)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/vai trò)"
STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+khóa)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Cục bộ (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Proxy (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="Đăng ký OAuth (ChatGPT)"
STR_AUTH_INTRO="Xác thực cho"
STR_AUTH_PROMPT="Nhập giá trị (Enter — để trống, điền sau)."
STR_AUTH_CURRENT_HINT="(hiện tại: <đã ẩn>)"
STR_DONE_TITLE="Thiết lập ban đầu hoàn tất"
STR_DONE_CONFIG="cấu hình:"
STR_DONE_SECRETS="bí mật:"
STR_MENU_TITLE="Trình cài đặt KeiSeiKit"
STR_MENU_SUBSTRATE="Nền tảng substrate (luôn cài đặt):"
STR_MENU_PROFILE_PROMPT="Chọn hồ sơ cài đặt:"
STR_MENU_CONFIRM="Xác nhận lựa chọn?"
STR_PREFLIGHT_FAILED="Preflight thất bại — nhà cung cấp có thể không hoạt động."
STR_PREFLIGHT_CONTINUE="Vẫn tiếp tục? [y/N]"

35
install/i18n/zh.sh Normal file
View file

@ -0,0 +1,35 @@
# shellcheck shell=bash
# i18n/zh.sh — 简体中文 (Simplified Chinese).
STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
STR_ONBOARDING_INTRO="初始化向导 (5 步)"
STR_PICK_LANGUAGE="选择界面语言:"
STR_PICK_TRANSPORT="选择连接方式:"
STR_PICK_PROVIDER="选择此分组中的提供者"
STR_PICK_MODEL="默认模型:"
STR_TR_DIRECT_API="提供者直连 API (密钥)"
STR_TR_AWS_BEDROCK="AWS Bedrock (IAM/角色)"
STR_TR_AZURE_OPENAI="Azure OpenAI (部署 + 密钥)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="本地 (Ollama/MLX/LMStudio)"
STR_TR_PROXY="代理 (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="OAuth 订阅 (ChatGPT)"
STR_AUTH_INTRO="认证"
STR_AUTH_PROMPT="输入值 (回车 — 保留为空,稍后填写)。"
STR_AUTH_CURRENT_HINT="(当前: <已隐藏>)"
STR_DONE_TITLE="初始化完成"
STR_DONE_CONFIG="配置:"
STR_DONE_SECRETS="密钥:"
STR_MENU_TITLE="KeiSeiKit 安装器"
STR_MENU_SUBSTRATE="基础组件 (始终安装):"
STR_MENU_PROFILE_PROMPT="选择安装配置:"
STR_MENU_CONFIRM="确认选择?"
STR_PREFLIGHT_FAILED="Preflight 失败 — 提供者可能无法工作。"
STR_PREFLIGHT_CONTINUE="仍然继续? [y/N]"

65
install/lib-i18n.sh Normal file
View file

@ -0,0 +1,65 @@
# shellcheck shell=bash
# lib-i18n.sh — лоадер локализаций.
#
# Контракт:
# 1. На старте всегда source install/i18n/en.sh — экран приветствия
# показывается ДО выбора языка пользователем.
# 2. После onboarding_pick_language вызывается i18n_load_lang "$lang" —
# перегружает строки выбранного словаря.
# 3. Любая строка отсутствующая в словаре — fallback на en.sh уже в
# памяти (мы не unset'им переменные, ru перезаписывает поверх).
#
# Используется install.sh и install/lib-onboarding.sh.
# Корень i18n относительно LIB_DIR.
I18N_DIR="${LIB_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}/i18n"
i18n_load_default() {
# shellcheck source=install/i18n/en.sh
source "$I18N_DIR/en.sh"
}
i18n_load_lang() {
local lang="$1"
# Сначала всегда грузим английский — это база fallback.
i18n_load_default
# Если выбран не-английский — поверх кладём словарь языка.
# Любой STR_*, отсутствующий в файле, остаётся с английским значением.
if [ "$lang" != "en" ] && [ -f "$I18N_DIR/${lang}.sh" ]; then
# shellcheck disable=SC1090
source "$I18N_DIR/${lang}.sh"
fi
}
# Список доступных языков — для onboarding_pick_language.
# Формат: <code>\t<display_name>
i18n_available_languages() {
cat <<'EOF'
en English
ru Русский
uk Українська
de Deutsch
fr Français
es Español
pt Português
it Italiano
tr Türkçe
ar العربية
hi हिन्दी
zh 简体中文
ja 日本語
ko 한국어
id Bahasa Indonesia
vi Tiếng Việt
EOF
}
# Welcome banner. Всегда EN. Запускается из install.sh до мастера.
i18n_print_welcome() {
echo ""
echo " ╔═══════════════════════════════════════════════════════╗"
echo "${STR_WELCOME_TITLE}"
echo "${STR_WELCOME_TAGLINE}"
echo " ╚═══════════════════════════════════════════════════════╝"
echo ""
}

View file

@ -30,8 +30,8 @@ menu_should_skip() {
# Profile choice = how many ADDITIONAL primitive binaries to add on top. # Profile choice = how many ADDITIONAL primitive binaries to add on top.
menu_whiptail_profile() { menu_whiptail_profile() {
local tool="$1" local tool="$1"
"$tool" --title "KeiSeiKit Installer — substrate always installed; profile = primitives ADDED on top" --radiolist \ "$tool" --title "${STR_MENU_TITLE:-KeiSeiKit Installer}${STR_MENU_SUBSTRATE:-substrate always installed; profile = primitives ADDED on top}" --radiolist \
"Choose install profile (SPACE to select, ENTER to confirm):" 28 86 12 \ "${STR_MENU_PROFILE_PROMPT:-Choose install profile (SPACE to select, ENTER to confirm):}" 28 86 12 \
"minimal" "substrate only — 0 primitives (~5s)" ON \ "minimal" "substrate only — 0 primitives (~5s)" ON \
"core" "+ 2 primitives (tomd, kei-doctor) (~5s)" OFF \ "core" "+ 2 primitives (tomd, kei-doctor) (~5s)" OFF \
"frontend" "+ 8 site tools — mock-render, visual-diff, figma-tokens" OFF \ "frontend" "+ 8 site tools — mock-render, visual-diff, figma-tokens" OFF \
@ -69,15 +69,15 @@ menu_whiptail_custom() {
# plain-text profile picker → profile name. Exits 1 on cancel. # plain-text profile picker → profile name. Exits 1 on cancel.
menu_plain_profile() { menu_plain_profile() {
echo "============================================================" >&2 echo "============================================================" >&2
echo " KeiSeiKit Installer" >&2 echo " ${STR_MENU_TITLE:-KeiSeiKit Installer}" >&2
echo "============================================================" >&2 echo "============================================================" >&2
echo >&2 echo >&2
echo " Substrate baseline (ALWAYS installed, regardless of profile):" >&2 echo " ${STR_MENU_SUBSTRATE:-Substrate baseline (ALWAYS installed):}" >&2
echo " • 37 agent manifests • 67 skills • 39 hooks" >&2 echo " • 37 agent manifests • 67 skills • 39 hooks" >&2
echo " • 82 blocks • 16 caps • 7 roles" >&2 echo " • 82 blocks • 16 caps • 7 roles" >&2
echo " • 11 cross-tool bridges (Cursor / Copilot / Codex / Aider / …)" >&2 echo " • 11 cross-tool bridges (Cursor / Copilot / Codex / Aider / …)" >&2
echo >&2 echo >&2
echo " Profile = primitive binaries ADDED on top of substrate." >&2 echo " ${STR_MENU_PROFILE_PROMPT:-Profile = primitive binaries ADDED on top of substrate.}" >&2
echo "------------------------------------------------------------" >&2 echo "------------------------------------------------------------" >&2
echo >&2 echo >&2
echo " Standard:" >&2 echo " Standard:" >&2

View file

@ -0,0 +1,79 @@
# shellcheck shell=bash
# lib-onboarding-registry.sh — парсеры реестров providers.toml + models.toml.
#
# Constructor Pattern: 1 файл = парсинг реестров. UI и state — в соседних кубах.
#
# Источник: $KIT_DIR/_blocks/registries/{providers,models}.toml (submodule
# kei-registries). Если файла нет — fallback на захардкоженный набор
# покрывающий все 7 транспортов.
#
# Глобалы (общие с lib-onboarding-*):
# REGISTRY_PROVIDERS — путь к providers.toml
# REGISTRY_MODELS — путь к models.toml
REGISTRY_PROVIDERS="${REGISTRY_PROVIDERS:-$KIT_DIR/_blocks/registries/providers.toml}"
REGISTRY_MODELS="${REGISTRY_MODELS:-$KIT_DIR/_blocks/registries/models.toml}"
# Парсер providers.toml. Простой awk-граббер по [[provider]] секциям.
# Печатает: <id>\t<transport>\t<display_name>\t<auth_env>
onboarding_list_providers() {
[ -f "$REGISTRY_PROVIDERS" ] || { onboarding_fallback_providers; return; }
awk '
/^\[\[provider\]\]/ { id=""; tr=""; dn=""; ae=""; next }
/^id[[:space:]]*=/ { gsub(/^id[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); id=$0 }
/^transport[[:space:]]*=/ { gsub(/^transport[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); tr=$0 }
/^display_name[[:space:]]*=/ { gsub(/^display_name[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); dn=$0 }
/^auth_env[[:space:]]*=/ { gsub(/^auth_env[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); ae=$0;
if (id && tr) print id "\t" tr "\t" dn "\t" ae }
' "$REGISTRY_PROVIDERS"
}
# Fallback если submodule не подтянут.
# Покрывает 7 транспортов минимальными представителями. Синхронизировать
# вручную если в реестре появится новый транспорт-тип.
onboarding_fallback_providers() {
printf "anthropic\tdirect-api\tAnthropic (Direct API)\tANTHROPIC_API_KEY\n"
printf "anthropic-bedrock\taws-bedrock\tAnthropic (AWS Bedrock)\tAWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_REGION\n"
printf "openai\tdirect-api\tOpenAI (Direct API)\tOPENAI_API_KEY\n"
printf "openai-azure\tazure-openai\tOpenAI (Azure)\tAZURE_OPENAI_API_KEY,AZURE_OPENAI_ENDPOINT,AZURE_OPENAI_DEPLOYMENT\n"
printf "xai\tdirect-api\txAI\tXAI_API_KEY\n"
printf "deepseek\tdirect-api\tDeepSeek\tDEEPSEEK_API_KEY\n"
printf "google\tdirect-api\tGoogle Gemini (Direct API)\tGEMINI_API_KEY\n"
printf "google-vertex\tgoogle-vertex\tGoogle Gemini (Vertex AI)\tGOOGLE_APPLICATION_CREDENTIALS,GCP_PROJECT_ID,GCP_REGION\n"
printf "ollama-local\tlocal\tOllama (local)\t_\n"
printf "mlx-local\tlocal\tMLX (Apple silicon local)\t_\n"
printf "lmstudio-local\tlocal\tLM Studio (local)\t_\n"
printf "litellm-proxy\tproxy\tLiteLLM proxy (keisei.app)\tKEI_LITELLM_KEY\n"
printf "openrouter\tproxy\tOpenRouter\tOPENROUTER_API_KEY\n"
printf "codex\tsubscription\tOpenAI Codex (ChatGPT OAuth)\t_\n"
}
# Уникальные транспорты — для первого экрана выбора.
onboarding_list_transports() {
onboarding_list_providers | awk -F'\t' '{print $2}' | sort -u
}
# Провайдеры внутри транспорта.
onboarding_providers_in_transport() {
local tr="$1"
onboarding_list_providers | awk -F'\t' -v t="$tr" '$2==t {print $1 "\t" $3 "\t" $4}'
}
# Модели по provider_ref.
onboarding_models_for_provider() {
local pr="$1"
[ -f "$REGISTRY_MODELS" ] || { printf "claude-sonnet-4-6\tClaude Sonnet 4.6\n"; return; }
awk -v pr="$pr" '
/^\[\[model\]\]/ { id=""; pref=""; dn=""; next }
/^id[[:space:]]*=/ { gsub(/^id[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); id=$0 }
/^provider_ref[[:space:]]*=/ { gsub(/^provider_ref[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); pref=$0 }
/^display_name[[:space:]]*=/ { gsub(/^display_name[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); dn=$0;
if (pref==pr) print id "\t" dn }
' "$REGISTRY_MODELS"
}
# auth_env для одного провайдера (для onboarding_collect_auth).
onboarding_auth_env_for_provider() {
local p="$1"
onboarding_list_providers | awk -F'\t' -v p="$p" '$1==p {print $4}'
}

View file

@ -0,0 +1,57 @@
# shellcheck shell=bash
# lib-onboarding-state.sh — запись результата мастера на диск.
#
# Constructor Pattern: 1 файл = state-запись. UI — в ui.sh, парсеры — в registry.sh.
#
# Пишет:
# ~/.claude/.onboarded — флаг прохождения
# ~/.claude/config/onboarding.toml — выбор lang/transport/provider/model
# ~/.claude/config/user-model-override.toml — для kei-model-router (HIGH аудит-1)
# ~/.claude/secrets/.env — добавляет ключи провайдера (chmod 600)
ONBOARDED_FLAG="${ONBOARDED_FLAG:-$HOME/.claude/.onboarded}"
ONBOARDING_CONFIG="${ONBOARDING_CONFIG:-$HOME/.claude/config/onboarding.toml}"
SECRETS_ENV="${SECRETS_ENV:-$HOME/.claude/secrets/.env}"
onboarding_write_secrets() {
[ "${#ONBOARDING_AUTH_ENV_KEYS[@]}" = "0" ] && return
mkdir -p "$(dirname "$SECRETS_ENV")"
touch "$SECRETS_ENV"; chmod 600 "$SECRETS_ENV"
local i
for i in "${!ONBOARDING_AUTH_ENV_KEYS[@]}"; do
local k="${ONBOARDING_AUTH_ENV_KEYS[$i]}"
local v="${ONBOARDING_AUTH_ENV_VALUES[$i]}"
if grep -q "^${k}=" "$SECRETS_ENV" 2>/dev/null; then
grep -v "^${k}=" "$SECRETS_ENV" > "$SECRETS_ENV.tmp"
mv "$SECRETS_ENV.tmp" "$SECRETS_ENV"
fi
printf '%s=%s\n' "$k" "$v" >> "$SECRETS_ENV"
done
chmod 600 "$SECRETS_ENV"
}
onboarding_write_config() {
mkdir -p "$(dirname "$ONBOARDING_CONFIG")"
cat > "$ONBOARDING_CONFIG" <<EOF
# KeiSeiKit onboarding choices. Auto-generated by lib-onboarding.sh.
# Re-run wizard: rm ~/.claude/.onboarded && ./install.sh
language = "$ONBOARDING_LANG"
transport = "$ONBOARDING_TRANSPORT"
provider = "$ONBOARDING_PROVIDER"
default_model = "$ONBOARDING_MODEL"
EOF
# Override для kei-model-router (HIGH аудит-1).
# Приоритет: --pinned flag > этот файл > agent-profiles.toml default_model_ref.
local override_path="$HOME/.claude/config/user-model-override.toml"
cat > "$override_path" <<EOF
# User-tier model override. Auto-generated by onboarding wizard.
# Format: kei-model-router::Registry::load_user_override().
# Priority: --pinned flag > этот файл > agent-profiles.toml default_model_ref.
provider = "$ONBOARDING_PROVIDER"
model = "$ONBOARDING_MODEL"
transport = "$ONBOARDING_TRANSPORT"
EOF
: > "$ONBOARDED_FLAG"
}

View file

@ -0,0 +1,189 @@
# shellcheck shell=bash
# lib-onboarding-ui.sh — pick_* функции мастера (whiptail / bash select).
#
# Constructor Pattern: 1 файл = UI слой. Парсеры реестров — в registry.sh,
# state-запись — в state.sh.
#
# Заполняет глобалы:
# ONBOARDING_LANG, ONBOARDING_TRANSPORT, ONBOARDING_PROVIDER, ONBOARDING_MODEL
# ONBOARDING_AUTH_ENV_KEYS[] + ONBOARDING_AUTH_ENV_VALUES[]
#
# Использует:
# - lib-i18n.sh: STR_* словарь + i18n_available_languages + i18n_load_lang
# - lib-onboarding-registry.sh: списки провайдеров/моделей
onboarding_pick_language() {
local langs
langs="$(i18n_available_languages 2>/dev/null)"
if [ -z "$langs" ]; then
langs="$(printf 'en\tEnglish\nru\tРусский\n')"
fi
if command -v whiptail >/dev/null 2>&1; then
local args=() first=1
while IFS=$'\t' read -r code name; do
[ -z "$code" ] && continue
if [ "$first" = "1" ]; then
args+=("$code" "$name" "ON"); first=0
else
args+=("$code" "$name" "OFF")
fi
done <<< "$langs"
ONBOARDING_LANG=$(whiptail --title "KeiSei · Language / Язык / 语言 / 言語 / ..." --radiolist \
"Choose interface language / Выберите язык:" 22 70 16 \
"${args[@]}" 3>&1 1>&2 2>&3) || ONBOARDING_LANG="en"
else
echo "" >&2
echo "Choose language / Выберите язык / 选择语言 / 言語選択:" >&2
declare -a codes=()
local i=1
while IFS=$'\t' read -r code name; do
[ -z "$code" ] && continue
codes+=("$code")
printf " %2d) %s — %s\n" "$i" "$code" "$name" >&2
i=$((i+1))
done <<< "$langs"
read -r -p "[1-${#codes[@]}, default 1=en]: " ans
ans="${ans:-1}"
ONBOARDING_LANG="${codes[$((ans-1))]:-en}"
fi
command -v i18n_load_lang >/dev/null 2>&1 && i18n_load_lang "$ONBOARDING_LANG"
}
onboarding_pick_transport() {
local transports
transports=$(onboarding_list_transports)
local prompt="${STR_PICK_TRANSPORT:-Choose connection transport:}"
if command -v whiptail >/dev/null 2>&1; then
local args=()
while IFS= read -r tr; do
local desc
case "$tr" in
direct-api) desc="${STR_TR_DIRECT_API:-Direct provider API}" ;;
aws-bedrock) desc="${STR_TR_AWS_BEDROCK:-AWS Bedrock}" ;;
azure-openai) desc="${STR_TR_AZURE_OPENAI:-Azure OpenAI}" ;;
google-vertex) desc="${STR_TR_GOOGLE_VERTEX:-Google Vertex AI}" ;;
local) desc="${STR_TR_LOCAL:-Local}" ;;
proxy) desc="${STR_TR_PROXY:-Proxy}" ;;
subscription) desc="${STR_TR_SUBSCRIPTION:-OAuth subscription}" ;;
*) desc="$tr" ;;
esac
args+=("$tr" "$desc" "OFF")
done <<< "$transports"
ONBOARDING_TRANSPORT=$(whiptail --title "KeiSei · Transport" --radiolist \
"$prompt" 18 70 7 "${args[@]}" 3>&1 1>&2 2>&3) || ONBOARDING_TRANSPORT="direct-api"
else
echo "" >&2
echo "$prompt" >&2
local i=1
declare -a opts=()
while IFS= read -r tr; do
opts+=("$tr")
echo " $i) $tr" >&2
i=$((i+1))
done <<< "$transports"
read -r -p "[1-${#opts[@]}, default 1]: " ans
ans="${ans:-1}"
ONBOARDING_TRANSPORT="${opts[$((ans-1))]:-direct-api}"
fi
}
onboarding_pick_provider() {
local rows; rows=$(onboarding_providers_in_transport "$ONBOARDING_TRANSPORT")
local count; count=$(echo "$rows" | wc -l | tr -d ' ')
# Если провайдер один на транспорт — авто-выбор.
if [ "$count" = "1" ]; then
ONBOARDING_PROVIDER=$(echo "$rows" | awk -F'\t' '{print $1}')
return
fi
if command -v whiptail >/dev/null 2>&1; then
local args=()
while IFS=$'\t' read -r id dn ae; do
args+=("$id" "$dn" "OFF")
done <<< "$rows"
local prompt="${STR_PICK_PROVIDER:-Provider within} $ONBOARDING_TRANSPORT:"
ONBOARDING_PROVIDER=$(whiptail --title "KeiSei · Provider" --radiolist \
"$prompt" 16 70 8 "${args[@]}" 3>&1 1>&2 2>&3) \
|| ONBOARDING_PROVIDER=$(echo "$rows" | head -1 | awk -F'\t' '{print $1}')
else
echo "" >&2
echo "${STR_PICK_PROVIDER:-Provider within} $ONBOARDING_TRANSPORT:" >&2
declare -a ids=()
local i=1
while IFS=$'\t' read -r id dn ae; do
ids+=("$id")
echo " $i) $id$dn" >&2
i=$((i+1))
done <<< "$rows"
read -r -p "[1-${#ids[@]}, default 1]: " ans
ans="${ans:-1}"
ONBOARDING_PROVIDER="${ids[$((ans-1))]:-${ids[0]}}"
fi
}
onboarding_pick_model() {
# Для AWS/Azure/Vertex модели идут под parent-провайдером — мапим.
local lookup="$ONBOARDING_PROVIDER"
case "$ONBOARDING_PROVIDER" in
anthropic-bedrock) lookup="anthropic" ;;
openai-azure) lookup="openai" ;;
google-vertex) lookup="google" ;;
esac
local rows; rows=$(onboarding_models_for_provider "$lookup")
[ -z "$rows" ] && rows=$(printf "claude-sonnet-4-6\tClaude Sonnet 4.6 (fallback)\n")
if command -v whiptail >/dev/null 2>&1; then
local args=()
while IFS=$'\t' read -r id dn; do
args+=("$id" "$dn" "OFF")
done <<< "$rows"
ONBOARDING_MODEL=$(whiptail --title "KeiSei · Model" --radiolist \
"${STR_PICK_MODEL:-Default model:}" 16 70 8 "${args[@]}" 3>&1 1>&2 2>&3) \
|| ONBOARDING_MODEL=$(echo "$rows" | head -1 | awk -F'\t' '{print $1}')
else
echo "" >&2
echo "${STR_PICK_MODEL:-Models for} $lookup:" >&2
declare -a ids=()
local i=1
while IFS=$'\t' read -r id dn; do
ids+=("$id")
echo " $i) $id$dn" >&2
i=$((i+1))
done <<< "$rows"
read -r -p "[1-${#ids[@]}, default 1]: " ans
ans="${ans:-1}"
ONBOARDING_MODEL="${ids[$((ans-1))]:-${ids[0]}}"
fi
}
onboarding_collect_auth() {
ONBOARDING_AUTH_ENV_KEYS=()
ONBOARDING_AUTH_ENV_VALUES=()
local ae; ae=$(onboarding_auth_env_for_provider "$ONBOARDING_PROVIDER")
[ -z "$ae" ] || [ "$ae" = "_" ] && return # local / subscription — нет ключей
echo "" >&2
echo "${STR_AUTH_INTRO:-Auth for} $ONBOARDING_PROVIDER ($ae):" >&2
echo "${STR_AUTH_PROMPT:-Enter values (Enter — leave empty, fill later).}" >&2
local IFS_old="$IFS"; IFS=','
for key in $ae; do
IFS="$IFS_old"
local cur="${!key:-}"
local prompt_msg="$key"
[ -n "$cur" ] && prompt_msg="$key ${STR_AUTH_CURRENT_HINT:-(current: <hidden>)}"
read -r -s -p " $prompt_msg = " val
echo "" >&2
if [ -n "$val" ]; then
ONBOARDING_AUTH_ENV_KEYS+=("$key")
ONBOARDING_AUTH_ENV_VALUES+=("$val")
elif [ -n "$cur" ]; then
ONBOARDING_AUTH_ENV_KEYS+=("$key")
ONBOARDING_AUTH_ENV_VALUES+=("$cur")
fi
done
IFS="$IFS_old"
}

95
install/lib-onboarding.sh Normal file
View file

@ -0,0 +1,95 @@
# shellcheck shell=bash
# lib-onboarding.sh — мастер первичной настройки (тонкий оркестратор).
#
# Иерархия: язык → транспорт → провайдер → модель → preflight → ключи.
#
# Constructor Pattern: этот файл — только координация. Логика по слоям:
# lib-onboarding-registry.sh — парсеры providers/models.toml + fallback
# lib-onboarding-ui.sh — pick_* функции (whiptail/bash select)
# lib-onboarding-state.sh — запись secrets/.env + onboarding.toml + флаг
# lib-preflight.sh — провайдер-специфичные CLI-проверки
# lib-i18n.sh — STR_* словарь + load_lang
#
# Источник: $KIT_DIR/_blocks/registries/{providers,models}.toml (submodule
# kei-registries). Если submodule не подтянут — fallback (см. registry.sh).
#
# Skip: $ONBOARDED_FLAG, env KEISEI_SKIP_ONBOARD=1, non-TTY.
# Глобалы заполняемые мастером.
ONBOARDING_LANG=""
ONBOARDING_TRANSPORT=""
ONBOARDING_PROVIDER=""
ONBOARDING_MODEL=""
declare -a ONBOARDING_AUTH_ENV_KEYS=()
declare -a ONBOARDING_AUTH_ENV_VALUES=()
ONBOARDED_FLAG="$HOME/.claude/.onboarded"
ONBOARDING_CONFIG="$HOME/.claude/config/onboarding.toml"
SECRETS_ENV="$HOME/.claude/secrets/.env"
REGISTRY_PROVIDERS="$KIT_DIR/_blocks/registries/providers.toml"
REGISTRY_MODELS="$KIT_DIR/_blocks/registries/models.toml"
# Подкубы (sourced параллельно — функции расходятся по namespace без коллизий).
# shellcheck source=install/lib-onboarding-registry.sh
[ -f "$LIB_DIR/lib-onboarding-registry.sh" ] && source "$LIB_DIR/lib-onboarding-registry.sh"
# shellcheck source=install/lib-onboarding-ui.sh
[ -f "$LIB_DIR/lib-onboarding-ui.sh" ] && source "$LIB_DIR/lib-onboarding-ui.sh"
# shellcheck source=install/lib-onboarding-state.sh
[ -f "$LIB_DIR/lib-onboarding-state.sh" ] && source "$LIB_DIR/lib-onboarding-state.sh"
# Skip-логика.
onboarding_should_run() {
[ -f "$ONBOARDED_FLAG" ] && return 1
[ "${KEISEI_SKIP_ONBOARD:-}" = "1" ] && return 1
[ ! -t 0 ] && return 1
[ ! -t 1 ] && return 1
return 0
}
# Оркестратор: 5 шагов + preflight + запись.
onboarding_run() {
onboarding_should_run || return 0
if command -v say >/dev/null 2>&1; then
say "${STR_ONBOARDING_INTRO:-Onboarding wizard (5 steps)}"
else
echo "── KeiSei: ${STR_ONBOARDING_INTRO:-onboarding (5 steps)} ──" >&2
fi
onboarding_pick_language
onboarding_pick_transport
onboarding_pick_provider
onboarding_pick_model
# Preflight — провайдер-специфичная проверка CLI/daemon до сбора ключей.
if command -v preflight_run >/dev/null 2>&1; then
if ! preflight_run "$ONBOARDING_PROVIDER"; then
echo "" >&2
echo "${STR_PREFLIGHT_FAILED:-Preflight failed — provider may not work.}" >&2
if [ -t 0 ] && [ -t 1 ]; then
read -r -p " ${STR_PREFLIGHT_CONTINUE:-Continue anyway? [y/N]} " _ans
case "$_ans" in
y|Y|yes|да|Да)
echo " → продолжаю; ключи запишутся но runtime может упасть." >&2
;;
*)
echo " → прервано; флаг .onboarded НЕ выставляется, перезапустите." >&2
return 1
;;
esac
else
echo " → non-TTY, продолжаю — настройте CLI вручную потом." >&2
fi
fi
fi
onboarding_collect_auth
onboarding_write_secrets
onboarding_write_config
if command -v say >/dev/null 2>&1; then
say "${STR_DONE_TITLE:-onboarding complete}: $ONBOARDING_TRANSPORT / $ONBOARDING_PROVIDER / $ONBOARDING_MODEL"
say " ${STR_DONE_CONFIG:-config:} $ONBOARDING_CONFIG"
[ "${#ONBOARDING_AUTH_ENV_KEYS[@]}" -gt 0 ] && say " ${STR_DONE_SECRETS:-secrets:} $SECRETS_ENV (chmod 600)"
fi
}

102
install/lib-preflight.sh Normal file
View file

@ -0,0 +1,102 @@
# shellcheck shell=bash
# lib-preflight.sh — диспетчер preflight-проверок CLI.
#
# Контракт:
# preflight_run <provider-id>
# 1. Ищет файл install/preflight/<provider-id>.sh
# 2. Если есть — source'ит и вызывает `preflight_check_<sanitized-id>`
# 3. Функция возвращает 0 (ok) / 1 (missing, инструкция напечатана)
# 4. Если файла нет — провайдеру CLI не нужен, тихо exit 0
#
# Файл per-provider должен экспортировать ОДНУ функцию:
# preflight_check_<id>() — печатает инструкцию в stderr, exit 0/1
#
# Sanitize: dashes в id заменяются на underscores для имени функции
# (bash не любит dashes в идентификаторах).
PREFLIGHT_DIR="${LIB_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}/preflight"
# Печатает инструкцию по установке, спрашивает действие.
# Аргументы: $1 — имя CLI, $2 — команда установки.
preflight_offer_install() {
local cli="$1"
local install_cmd="$2"
echo "" >&2
echo "$cli не найден." >&2
echo " Установить: $install_cmd" >&2
echo "" >&2
if [ -t 0 ] && [ -t 1 ]; then
echo " ⓘ команда: $install_cmd" >&2
read -r -p " Поставить сейчас? [y/N/skip] " ans
case "$ans" in
y|Y|yes)
# bash -c вместо eval — explicit subshell, не word-splitting'тся
# лишний раз в текущем процессе.
bash -c "$install_cmd"
return $?
;;
skip|s|S)
echo " пропускаю — поставите вручную позже." >&2
return 0
;;
*)
echo " пропуск (по умолчанию)." >&2
return 1
;;
esac
else
# non-TTY: только печатаем инструкцию.
return 1
fi
}
# Универсальный helper для типового CLI-чека (command -v + offer-install + version).
# Используется per-provider preflight файлами чтобы убрать boilerplate.
#
# Аргументы:
# $1 — имя CLI (для сообщений), например "aws CLI"
# $2 — бинарь (для command -v), например "aws"
# $3 — install_cmd (для preflight_offer_install)
# $4 — version_cmd (для печати при success), например "aws --version"
#
# Возврат: 0 если CLI есть, 1 если нет и юзер не поставил.
preflight_check_cli() {
local label="$1"
local bin="$2"
local install_cmd="$3"
local version_cmd="$4"
if ! command -v "$bin" >/dev/null 2>&1; then
preflight_offer_install "$label" "$install_cmd" || return 1
# После install проверяем что бинарь появился в PATH.
command -v "$bin" >/dev/null 2>&1 || return 1
fi
echo "$label: $(eval "$version_cmd" 2>&1 | head -1)" >&2
return 0
}
# Главный диспетчер. Вызывается из onboarding между pick_model и collect_auth.
preflight_run() {
local provider="$1"
[ -z "$provider" ] && return 0
# Whitelist символов в provider-id: только [a-z0-9_-], длина 1..64.
# Защищает от path-traversal (../) и shell-инъекций через имя файла.
if ! [[ "$provider" =~ ^[a-z0-9][a-z0-9_-]{0,63}$ ]]; then
echo " ⚠ preflight: provider id '$provider' содержит недопустимые символы — пропуск" >&2
return 0
fi
local script="$PREFLIGHT_DIR/${provider}.sh"
# Проверяем что resolved путь не вышел за PREFLIGHT_DIR (на случай symlink'ов).
local resolved
resolved="$(cd "$PREFLIGHT_DIR" 2>/dev/null && pwd -P)/${provider}.sh"
if [ ! -f "$script" ] || [ ! -f "$resolved" ]; then
return 0 # CLI не нужен — direct-api, ключ собирается ниже
fi
# shellcheck disable=SC1090
source "$script"
local fn="preflight_check_${provider//-/_}"
if command -v "$fn" >/dev/null 2>&1; then
"$fn"
return $?
fi
return 0
}

View file

@ -0,0 +1,40 @@
# shellcheck shell=bash
# preflight/anthropic-bedrock.sh — AWS CLI + Bedrock региональный доступ.
preflight_check_anthropic_bedrock() {
if ! command -v aws >/dev/null 2>&1; then
local cmd
case "$(uname -s)" in
Darwin) cmd="brew install awscli" ;;
Linux) cmd="curl 'https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip' -o /tmp/awscliv2.zip && unzip -q /tmp/awscliv2.zip -d /tmp && sudo /tmp/aws/install" ;;
*) cmd="см. https://aws.amazon.com/cli/" ;;
esac
preflight_offer_install "aws CLI" "$cmd" || return 1
fi
# Один вызов вместо двух — экономит ~1-3с при success-path.
local identity_out identity_rc
identity_out="$(aws sts get-caller-identity --output json 2>&1)"
identity_rc=$?
if [ $identity_rc -ne 0 ]; then
echo "" >&2
# Различаем cred-ошибку от сетевой/прочей по тексту.
if echo "$identity_out" | grep -qiE "UnauthorizedAccess|InvalidClientTokenId|ExpiredToken|signature|credential"; then
echo " ⚠ AWS credentials невалидны." >&2
echo " Запустите: aws configure" >&2
echo " Или экспортируйте AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY + AWS_REGION." >&2
else
echo " ⚠ aws sts get-caller-identity упал (не credentials)." >&2
echo " raw: $identity_out" >&2
fi
echo "" >&2
return 1
fi
echo " ✓ aws CLI: $(aws --version 2>&1 | head -1)" >&2
# ARN не печатаем полностью — может содержать account-id (sensitive enum target).
# Показываем только тип identity (user/role/assumed-role) и user-name.
local arn_short
arn_short="$(echo "$identity_out" | sed -n 's/.*"Arn": *"\(arn:aws:[^:]*::\)[0-9]*\(:[^"]*\)".*/\1***\2/p')"
[ -z "$arn_short" ] && arn_short="<masked>"
echo " ✓ identity: $arn_short" >&2
return 0
}

View file

@ -0,0 +1,41 @@
# shellcheck shell=bash
# preflight/codex.sh — OpenAI Codex CLI через ChatGPT OAuth.
preflight_check_codex() {
if ! command -v codex >/dev/null 2>&1; then
if ! command -v npm >/dev/null 2>&1; then
echo "" >&2
echo " ⚠ npm требуется для установки codex." >&2
echo " Сначала: brew install node (macOS) или apt install nodejs npm (Linux)" >&2
echo "" >&2
return 1
fi
preflight_offer_install "codex CLI" "npm install -g @openai/codex" || return 1
fi
# Проверяем что OAuth активен.
# Regex'ы: позитивные паттерны (logged-in / signed-in / active) И
# негативные (not logged in / logged out / sign in required) — иначе
# фраза "you are not logged in" проходит через `logged.in` regex
# как false-pass.
local status
status="$(codex login status 2>&1 || true)"
if echo "$status" | grep -qiE 'not (logged|signed) (in|on)|logged out|signed out|please.*log\s*in|sign[[:space:]]*in[[:space:]]*required'; then
echo "" >&2
echo " ⚠ codex не залогинен в ChatGPT." >&2
echo " Запустите: codex login" >&2
echo " (требуется ChatGPT Plus/Pro/Team подписка)" >&2
echo "" >&2
return 1
fi
if ! echo "$status" | grep -qiE '\b(logged|signed) in\b|status:[[:space:]]*active|auth(enticated)?[[:space:]]*:[[:space:]]*(yes|true|ok)'; then
echo "" >&2
echo " ⚠ codex auth-статус неопределён:" >&2
echo " $status" >&2
echo " Запустите: codex login" >&2
echo "" >&2
return 1
fi
echo " ✓ codex CLI: $(codex --version 2>&1 | head -1)" >&2
echo " ✓ OAuth: active" >&2
return 0
}

View file

@ -0,0 +1,34 @@
# shellcheck shell=bash
# preflight/google-vertex.sh — gcloud CLI + service-account JSON.
preflight_check_google_vertex() {
if ! command -v gcloud >/dev/null 2>&1; then
local cmd
case "$(uname -s)" in
Darwin) cmd="brew install --cask google-cloud-sdk" ;;
Linux)
echo " ⚠ Linux установка gcloud тянет скрипт с sdk.cloud.google.com" >&2
echo " и выполняет его как bash — без проверки хэша." >&2
echo " Альтернатива: пакет из репов apt/dnf, либо ручная установка по" >&2
echo " https://cloud.google.com/sdk/docs/install#linux" >&2
cmd="curl https://sdk.cloud.google.com | bash"
;;
*) cmd="см. https://cloud.google.com/sdk/docs/install" ;;
esac
preflight_offer_install "gcloud CLI" "$cmd" || return 1
fi
# Проверяем что выбран project.
local project
project="$(gcloud config get-value project 2>/dev/null)"
if [ -z "$project" ] || [ "$project" = "(unset)" ]; then
echo "" >&2
echo " ⚠ GCP project не выбран." >&2
echo " Запустите: gcloud auth login && gcloud config set project YOUR_PROJECT_ID" >&2
echo " Также установите GOOGLE_APPLICATION_CREDENTIALS на путь к service-account JSON." >&2
echo "" >&2
return 1
fi
echo " ✓ gcloud CLI: $(gcloud --version 2>&1 | head -1)" >&2
echo " ✓ project: $project" >&2
return 0
}

View file

@ -0,0 +1,16 @@
# shellcheck shell=bash
# preflight/lmstudio-local.sh — LM Studio desktop GUI на 127.0.0.1:1234.
preflight_check_lmstudio_local() {
# LM Studio это desktop-приложение, не CLI — проверяем только порт.
if ! curl -fsS --max-time 3 http://127.0.0.1:1234/v1/models >/dev/null 2>&1; then
echo "" >&2
echo " ⚠ LM Studio сервер не запущен на 1234." >&2
echo " Скачайте: https://lmstudio.ai/" >&2
echo " В GUI: Local Server → Start Server (порт 1234 по умолчанию)" >&2
echo "" >&2
return 1
fi
echo " ✓ LM Studio: 127.0.0.1:1234 отвечает" >&2
return 0
}

View file

@ -0,0 +1,24 @@
# shellcheck shell=bash
# preflight/mlx-local.sh — MLX inference server (Apple silicon).
preflight_check_mlx_local() {
if [ "$(uname -s)" != "Darwin" ] || [ "$(uname -m)" != "arm64" ]; then
echo "" >&2
echo " ⚠ MLX доступен только на Apple silicon (arm64 macOS)." >&2
echo " Текущая платформа: $(uname -s) $(uname -m)" >&2
return 1
fi
if ! command -v mlx_lm.server >/dev/null 2>&1; then
# --user избегает sudo и не загрязняет system Python.
preflight_offer_install "mlx_lm" "pip install --user mlx-lm" || return 1
fi
if ! curl -fsS --max-time 3 http://127.0.0.1:8080/v1/models >/dev/null 2>&1; then
echo "" >&2
echo " ⚠ MLX server не запущен на 8080." >&2
echo " Запустите: mlx_lm.server --model mlx-community/Qwen2.5-Coder-32B-Instruct-4bit" >&2
echo "" >&2
return 1
fi
echo " ✓ mlx_lm.server: 127.0.0.1:8080 отвечает" >&2
return 0
}

View file

@ -0,0 +1,34 @@
# shellcheck shell=bash
# preflight/ollama-local.sh — Ollama daemon на 127.0.0.1:11434.
preflight_check_ollama_local() {
if ! command -v ollama >/dev/null 2>&1; then
local cmd
case "$(uname -s)" in
Darwin) cmd="brew install ollama" ;;
Linux)
# WARNING: curl|sh без verification — supply-chain риск.
# Если есть apt/dnf — лучше через них, но ollama не в репах
# большинства дистров. Предупреждаем юзера явно.
echo " ⚠ Linux установка ollama тянет скрипт с https://ollama.com и" >&2
echo " выполняет его как shell — без проверки хэша/подписи." >&2
echo " Альтернатива: скачать вручную с https://ollama.com/download/linux" >&2
echo " и проверить SHA256 перед запуском." >&2
cmd="curl -fsSL https://ollama.com/install.sh | sh"
;;
*) cmd="см. https://ollama.com/download" ;;
esac
preflight_offer_install "ollama" "$cmd" || return 1
fi
# Проверяем что daemon запущен.
if ! curl -fsS --max-time 3 http://127.0.0.1:11434/api/tags >/dev/null 2>&1; then
echo "" >&2
echo " ⚠ ollama daemon не запущен." >&2
echo " Запустите: ollama serve (или brew services start ollama на macOS)" >&2
echo "" >&2
return 1
fi
echo " ✓ ollama: $(ollama --version 2>&1 | head -1)" >&2
echo " ✓ daemon: 127.0.0.1:11434 отвечает" >&2
return 0
}