""" Claude Code integration setup. Installs ssh.py, encryption.py, /ssh skill, SSH key — everything needed for Claude Code to manage servers via the shared servers.json. """ import os import sys import shutil from core.logger import log SHARED_DIR = os.path.expanduser("~/.server-connections") # PyInstaller: bundled data is in sys._MEIPASS; otherwise use project dir if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): _BASE_DIR = sys._MEIPASS else: _BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SSH_SCRIPT_SRC = os.path.join(_BASE_DIR, "tools", "ssh.py") ENCRYPTION_SRC = os.path.join(_BASE_DIR, "core", "encryption.py") SKILL_SRC = os.path.join(_BASE_DIR, "tools", "skill-ssh.md") SKILL_DST_DIR = os.path.expanduser("~/.claude/commands") SKILL_DST = os.path.join(SKILL_DST_DIR, "ssh.md") SSH_KEY_PATH = os.path.expanduser("~/.ssh/id_ed25519") GLOBAL_CLAUDE_MD = os.path.expanduser("~/.claude/CLAUDE.md") _BLOCK_START = "" _BLOCK_END = "" GLOBAL_CLAUDE_MD_BLOCK = f"""{_BLOCK_START} ## Server Manager — управление серверами **ВСЕГДА** используй server manager для подключения к серверам. Никогда не используй `ssh`, `sshpass` или прямые подключения. - Скилл: `/ssh ALIAS "command"` — выполнить команду на сервере - Список серверов: `python3 ~/.server-connections/ssh.py --list` - Документация: `~/.claude/commands/ssh.md` - Memory bank: проект `global-infrastructure` → `techContext.md` - Инфраструктура: https://git.sensey24.ru/aibot777/infrastructure-docs **Запрещено:** использовать `ssh`, `sshpass`, читать `~/.server-connections/` напрямую, раскрывать IP/пароли/порты. {_BLOCK_END} """ def check_status() -> dict: """Check what's installed and what's missing.""" return { "shared_dir": os.path.exists(SHARED_DIR), "servers_json": os.path.exists(os.path.join(SHARED_DIR, "servers.json")), "ssh_script": os.path.exists(os.path.join(SHARED_DIR, "ssh.py")), "encryption": os.path.exists(os.path.join(SHARED_DIR, "encryption.py")), "skill_installed": os.path.exists(SKILL_DST), "ssh_key_exists": os.path.exists(SSH_KEY_PATH), "ssh_key_pub": os.path.exists(SSH_KEY_PATH + ".pub"), } def install_ssh_script() -> str: """Copy ssh.py and encryption.py to shared dir.""" os.makedirs(SHARED_DIR, exist_ok=True) results = [] # Copy ssh.py dst = os.path.join(SHARED_DIR, "ssh.py") if os.path.exists(SSH_SCRIPT_SRC): shutil.copy2(SSH_SCRIPT_SRC, dst) log.info(f"ssh.py installed: {dst}") results.append(f"ssh.py installed: {dst}") elif os.path.exists(dst): results.append(f"ssh.py already exists: {dst}") else: results.append("ERROR: ssh.py source not found") # Copy encryption.py enc_dst = os.path.join(SHARED_DIR, "encryption.py") if os.path.exists(ENCRYPTION_SRC): shutil.copy2(ENCRYPTION_SRC, enc_dst) log.info(f"encryption.py installed: {enc_dst}") results.append(f"encryption.py installed: {enc_dst}") elif os.path.exists(enc_dst): results.append(f"encryption.py already exists: {enc_dst}") else: results.append("ERROR: encryption.py source not found") return "\n".join(results) def install_skill() -> str: """Install /ssh skill for Claude Code.""" os.makedirs(SKILL_DST_DIR, exist_ok=True) if os.path.exists(SKILL_SRC): shutil.copy2(SKILL_SRC, SKILL_DST) log.info(f"Skill installed: {SKILL_DST}") return f"Skill installed: {SKILL_DST}" # Fallback: check existing if os.path.exists(SKILL_DST): return f"Skill already exists: {SKILL_DST}" # Generate minimal skill skill_content = _generate_skill_content() with open(SKILL_DST, "w", encoding="utf-8") as f: f.write(skill_content) log.info(f"Skill generated: {SKILL_DST}") return f"Skill generated: {SKILL_DST}" def generate_ssh_key() -> str: """Generate ed25519 SSH key if not exists.""" if os.path.exists(SSH_KEY_PATH): return f"Key already exists: {SSH_KEY_PATH}" os.makedirs(os.path.dirname(SSH_KEY_PATH), exist_ok=True) import subprocess try: subprocess.run( ["ssh-keygen", "-t", "ed25519", "-f", SSH_KEY_PATH, "-N", "", "-C", "server-manager"], check=True, capture_output=True, timeout=15 ) log.info(f"SSH key generated: {SSH_KEY_PATH}") return f"Key generated: {SSH_KEY_PATH}" except FileNotFoundError: hint = "enable OpenSSH optional feature" if sys.platform == "win32" else "install openssh-client" msg = f"ERROR: ssh-keygen not found — {hint}" log.error(msg) return msg except subprocess.CalledProcessError as e: msg = f"ERROR: ssh-keygen failed: {e.stderr.decode().strip()}" log.error(msg) return msg def install_global_claude_md() -> str: """Add/update server manager section in global ~/.claude/CLAUDE.md. Uses start/end markers to safely replace existing block without duplication. """ import re os.makedirs(os.path.dirname(GLOBAL_CLAUDE_MD), exist_ok=True) existing = "" if os.path.exists(GLOBAL_CLAUDE_MD): with open(GLOBAL_CLAUDE_MD, encoding="utf-8") as f: existing = f.read() pattern = re.compile( re.escape(_BLOCK_START) + r".*?" + re.escape(_BLOCK_END), re.DOTALL ) if pattern.search(existing): # Блок уже есть — заменяем на актуальную версию updated = pattern.sub(GLOBAL_CLAUDE_MD_BLOCK.strip(), existing) with open(GLOBAL_CLAUDE_MD, "w", encoding="utf-8") as f: f.write(updated) log.info(f"Global CLAUDE.md block updated: {GLOBAL_CLAUDE_MD}") return f"Global CLAUDE.md block updated: {GLOBAL_CLAUDE_MD}" else: # Блока нет — добавляем в конец with open(GLOBAL_CLAUDE_MD, "a", encoding="utf-8") as f: if existing and not existing.endswith("\n"): f.write("\n") f.write("\n" + GLOBAL_CLAUDE_MD_BLOCK) log.info(f"Global CLAUDE.md block added: {GLOBAL_CLAUDE_MD}") return f"Global CLAUDE.md block added: {GLOBAL_CLAUDE_MD}" def install_all() -> list[str]: """Full setup — install everything.""" results = [] steps = [ ("ssh_script", install_ssh_script), ("skill", install_skill), ("ssh_key", generate_ssh_key), ("global_claude_md", install_global_claude_md), ] for name, func in steps: try: log.info(f"install_all: running {name}") result = func() results.append(result) except Exception as e: msg = f"ERROR ({name}): {e}" log.error(msg) results.append(msg) return results def _generate_skill_content() -> str: """Generate /ssh skill markdown (fallback if skill-ssh.md not found).""" return """# Скилл /ssh — управление удалёнными серверами Ты управляешь удалёнными серверами через SSH-утилиту. ## ВАЖНО — Безопасность - **НИКОГДА не читай файлы** в директории `~/.server-connections/` напрямую - **НИКОГДА не читай** файлы `encryption.py`, `servers.json`, `settings.json` - **НИКОГДА не выводи пароли, IP-адреса, логины, порты** пользователю и в контекст нейронки - **НИКОГДА не используй** `--list-full` — он выводит IP/порты/логины в контекст AI - **Все операции только через** `python ~/.server-connections/ssh.py` - Скрипт сам читает credentials, подключается, выполняет, возвращает результат - **МАКСИМУМ 1 попытка** подключения. Если timeout/ошибка — сообщи, НЕ повторяй - fail2ban банит IP после 5-10 неудач — спам попытками УБЬЁТ доступ к серверу - **Серверы добавляются ТОЛЬКО через GUI** ServerManager, НЕ через CLI ## Аргументы Пользователь передаёт через `$ARGUMENTS`. Разбери и выполни. ## Команды ### Список серверов (безопасный — alias, тип, ключ, заметки) ```bash python ~/.server-connections/ssh.py --list ``` ### Информация о сервере (безопасная — без IP/логина/пароля/порта) ```bash python ~/.server-connections/ssh.py --info ALIAS ``` ### Статус всех серверов (alias + online/offline) ```bash python ~/.server-connections/ssh.py --status ``` ### Выполнить команду на сервере ```bash python ~/.server-connections/ssh.py ALIAS "command" ``` ### Выполнить команду БЕЗ sudo ```bash python ~/.server-connections/ssh.py ALIAS --no-sudo "command" ``` ### Загрузить файл на сервер ```bash python ~/.server-connections/ssh.py ALIAS --upload "local/file" //remote/path/file ``` **ВАЖНО (Windows/Git Bash):** remote path ОБЯЗАТЕЛЬНО с двойным слешем `//home/...`, `//tmp/...`. ### Скачать файл с сервера ```bash python ~/.server-connections/ssh.py ALIAS --download //remote/path/file "local/file" ``` ### Установить SSH-ключ на сервер ```bash python ~/.server-connections/ssh.py ALIAS --install-key ``` ### Проверить доступность сервера ```bash python ~/.server-connections/ssh.py ALIAS --ping ``` ### Обновить заметки сервера ```bash python ~/.server-connections/ssh.py --set-note ALIAS "описание сервера" ``` ### Удалить сервер ```bash python ~/.server-connections/ssh.py --remove ALIAS ``` **Спроси подтверждение у пользователя перед удалением!** ## Поведение - **Auto-sudo**: если user не root — команды автоматически оборачиваются в `sudo -S` - **--no-sudo**: для команд без root (например `ls`, `cat`) - **Timeout**: 120 секунд на команду, 15 секунд на подключение - **SSH-ключ**: пробуется первым, fallback на пароль - **Прогресс**: файлы >=1MB показывают 25/50/75%, итог с размером/временем/скоростью ## Правила - Отвечай на русском языке - Показывай результат каждой операции - При ошибках — объясняй причину и предлагай решение - Если timeout — предложи проверить VPN/firewall/панель хостера - Файлы создаваемые на сервере должны иметь права 664 (owner+group rw) - При вопросе о серверах — СНАЧАЛА `--list`, потом `--info ALIAS` если нужны детали """