feat: install panel as launchd service, refuse install over SSH (#4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
88124cf6d8
commit
7958314547
@ -1,15 +1,17 @@
|
|||||||
# Архитектура
|
# Архитектура
|
||||||
|
|
||||||
Два процесса, связанные только файлами в `~/.cache/claude-agents/`: хук внутри каждой сессии
|
Три части, связанные файлами в `~/.cache/claude-agents/` и адресом `127.0.0.1:8766`: хук внутри
|
||||||
Claude Code пишет события, скрипт iTerm2 их читает и рисует панель.
|
каждой сессии Claude Code пишет события, служба launchd их читает и отдаёт страницу, скрипт iTerm2
|
||||||
|
показывает эту страницу в Toolbelt. iTerm2 для работы панели не нужен.
|
||||||
|
|
||||||
```
|
```
|
||||||
claude (каждая сессия) ──hook──▶ agents_panel_hook.sh ──append──▶ ~/.cache/claude-agents/<sid>.jsonl
|
claude (каждая сессия) ──hook──▶ agents_panel_hook.sh ──append──▶ ~/.cache/claude-agents/<sid>.jsonl
|
||||||
│
|
│
|
||||||
iTerm2 AutoLaunch: claude_agents.py ◀───── read (poll 1.5 s) ────────────┘
|
launchd: claude_agents.py --headless ◀──── read (poll 1.5 s) ────────────┘
|
||||||
├─ + iTerm2 API (вкладки, профиль), ps, tmux, транскрипты ~/.claude/projects, git
|
├─ + ps, tmux, транскрипты ~/.claude/projects, git
|
||||||
├─ + api.anthropic.com oauth/usage, prepaid/credits (OAuth из Keychain)
|
├─ + api.anthropic.com oauth/usage, prepaid/credits (OAuth из Keychain)
|
||||||
└─ HTTP 127.0.0.1:8766 ──▶ web view tool «Claude» в Toolbelt
|
└─ HTTP 127.0.0.1:8766 ──▶ web view «Claude» в Toolbelt (регистрирует claude_agents.py в iTerm2)
|
||||||
|
└─▶ браузер; с iPad — SSH local forward в Termius
|
||||||
```
|
```
|
||||||
|
|
||||||
## Модули
|
## Модули
|
||||||
@ -17,19 +19,23 @@ iTerm2 AutoLaunch: claude_agents.py ◀───── read (poll 1.5 s) ──
|
|||||||
| Модуль | Ответственность | Зависит от |
|
| Модуль | Ответственность | Зависит от |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `files/agents_panel_hook.sh` | Событие хука → строка jsonl с PID, вкладкой iTerm2 или панелью tmux | `jq`, `ps` |
|
| `files/agents_panel_hook.sh` | Событие хука → строка jsonl с PID, вкладкой iTerm2 или панелью tmux | `jq`, `ps` |
|
||||||
| `files/claude_agents.py` — сессии | `load_sessions`/`replay`: jsonl → состояние сессий и субагентов; `build_tree` сводит с живыми вкладками, процессами, tmux | файлы хука, iTerm2 API, `ps`, `tmux`, транскрипты, `git` |
|
| `files/claude_agents.py` — сессии | `load_sessions`/`replay`: jsonl → состояние сессий и субагентов; `build_tree` сводит с процессами и tmux | файлы хука, `ps`, `tmux`, транскрипты, `git` |
|
||||||
| `files/claude_agents.py` — лимиты | `fetch_usage`/`fetch_balance` + `parse_limits`/`parse_balance` | Keychain/`~/.claude/.credentials.json`, `~/.claude.json`, api.anthropic.com, claude.ai |
|
| `files/claude_agents.py` — лимиты | `fetch_usage`/`fetch_balance` + `parse_limits`/`parse_balance` | Keychain/`~/.claude/.credentials.json`, `~/.claude.json`, api.anthropic.com, claude.ai |
|
||||||
| `files/claude_agents.py` — UI | `render_*`, `handle` (HTTP), `activate`/`open_worktree`, `theme_poller` | `state`, iTerm2 API |
|
| `files/claude_agents.py` — UI | `render_*`, `handle` (HTTP), `activate`/`open_worktree` (tmux) | `state` |
|
||||||
|
| `files/claude_agents.py` — запуск | `run`: `serve` (служба, `--headless`) или `register_toolbelt` (iTerm2) | launchd, `iterm2` |
|
||||||
|
| `files/launchagent.py` | plist службы `com.andrey.claude-agents` | — |
|
||||||
| `files/settings_hooks.py` | Идемпотентно добавить/убрать хуки в `~/.claude/settings.json` | — |
|
| `files/settings_hooks.py` | Идемпотентно добавить/убрать хуки в `~/.claude/settings.json` | — |
|
||||||
| `install.sh` / `uninstall.sh` | Разложить/убрать файлы, включить Python API iTerm2, перезапустить панель | `settings_hooks.py` |
|
| `install.sh` / `uninstall.sh` | Разложить/убрать файлы, поставить/снять службу, включить Python API iTerm2 | `settings_hooks.py`, `launchagent.py`, `launchctl` |
|
||||||
|
|
||||||
## Границы
|
## Границы
|
||||||
- Хук не зависит от панели и не должен влиять на Claude Code: без вывода, всегда `exit 0`.
|
- Хук не зависит от панели и не должен влиять на Claude Code: без вывода, всегда `exit 0`.
|
||||||
- Панель только читает данные Claude Code (jsonl, транскрипты, `~/.claude.json`) и ничего туда не пишет;
|
- Панель только читает данные Claude Code (jsonl, транскрипты, `~/.claude.json`) и ничего туда не пишет;
|
||||||
её единственная запись — удаление собственных логов старше `keep_logs_days`.
|
её единственная запись — удаление собственных логов старше `keep_logs_days`.
|
||||||
- `claude_agents.py` — только стандартная библиотека + `iterm2`; импорт без iTerm2 должен работать.
|
- Порт занимает только служба; скрипт iTerm2 сервер не поднимает и данные iTerm2 не передаёт.
|
||||||
|
- `claude_agents.py` — только стандартная библиотека + опционально `iterm2`; импорт без iTerm2 должен работать.
|
||||||
- HTTP слушает только `127.0.0.1`.
|
- HTTP слушает только `127.0.0.1`.
|
||||||
|
- Фон панели выбирается на клиенте и хранится в браузере/web view; сервер о нём не знает.
|
||||||
|
|
||||||
## Открытые вопросы
|
## Открытые вопросы
|
||||||
- Docstring `claude_agents.py` ссылается на несуществующий `settings-hooks.json` (теперь это `settings_hooks.py` + `install.sh`).
|
- После перезагрузки Mac без входа на нём (только SSH) служба не стартует, Keychain заблокирован.
|
||||||
- Автотестов нет; `claude_agents.py` — один файл на 1500 строк.
|
- `claude_agents.py` — один файл на ~1400 строк.
|
||||||
|
|||||||
62
CLAUDE.md
62
CLAUDE.md
@ -1,55 +1,63 @@
|
|||||||
# cc-agents-iterm-plugin
|
# cc-agents-iterm-plugin
|
||||||
|
|
||||||
## Что это
|
## Что это
|
||||||
Панель «Claude» для iTerm2 Toolbelt. Сверху — дерево активных сессий Claude Code и их субагентов
|
Панель «Claude» — страница на `http://127.0.0.1:8766/`. Сверху — дерево активных сессий Claude Code
|
||||||
(название, модель, размер контекста, worktree, статус), снизу — лимиты подписки (5 ч, неделя,
|
и их субагентов (название, модель, размер контекста, worktree, статус), снизу — лимиты подписки
|
||||||
разбивка по продуктам) и баланс extra usage. Для одного пользователя на его Mac: видеть, что
|
(5 ч, неделя, разбивка по продуктам) и баланс extra usage. Её показывает iTerm2 Toolbelt и
|
||||||
делают все запущенные `claude` и сколько осталось лимитов, и в один клик переходить в нужную
|
браузер — в том числе Safari на iPad через SSH-туннель Termius. Для одного пользователя на его
|
||||||
вкладку или панель tmux.
|
Mac: видеть, что делают все запущенные `claude` (в tmux), сколько осталось лимитов, и в один клик
|
||||||
|
переходить в нужную панель tmux.
|
||||||
|
|
||||||
## Стек и ограничения
|
## Стек и ограничения
|
||||||
- Python 3.8+, только стандартная библиотека; модуль `iterm2` предоставляет сам iTerm2
|
- Python 3.8+, только стандартная библиотека; модуль `iterm2` нужен только регистратору Toolbelt
|
||||||
(скрипт AutoLaunch). Без зависимостей из pip — пользователь ставит панель `./install.sh`.
|
(его даёт iTerm2). Без зависимостей из pip — пользователь ставит панель `./install.sh`.
|
||||||
- Хук — POSIX `sh` + `jq`. Хук никогда не мешает Claude Code: ничего не выводит, всегда `exit 0`.
|
- Хук — POSIX `sh` + `jq`. Хук никогда не мешает Claude Code: ничего не выводит, всегда `exit 0`.
|
||||||
- Только macOS + iTerm2 с включённым Python API. Токен OAuth берётся из Keychain
|
- macOS, launchd. Токен OAuth берётся из Keychain (`Claude Code-credentials`) или
|
||||||
(`Claude Code-credentials`) или `~/.claude/.credentials.json`.
|
`~/.claude/.credentials.json`, поэтому служба работает в GUI-сессии пользователя.
|
||||||
|
- Сервер слушает только `127.0.0.1` — в панели видны тексты сообщений сессий.
|
||||||
- Установщик не трогает чужие хуки и настройки: правит `~/.claude/settings.json` только через
|
- Установщик не трогает чужие хуки и настройки: правит `~/.claude/settings.json` только через
|
||||||
`settings_hooks.py` (маркер `agents_panel_hook`), перед заменой кладёт копии в
|
`settings_hooks.py` (маркер `agents_panel_hook`), перед заменой кладёт копии в
|
||||||
`~/.claude-panel-backup/<дата>/`. Повторный запуск `install.sh` идемпотентен.
|
`~/.claude-panel-backup/<дата>/`. Повторный запуск `install.sh` идемпотентен. Запуск по SSH
|
||||||
|
`install.sh`/`uninstall.sh` отклоняют.
|
||||||
|
|
||||||
## Архитектура
|
## Архитектура
|
||||||
- `files/agents_panel_hook.sh` — хук Claude Code на SessionStart/End, UserPromptSubmit, Stop,
|
- `files/agents_panel_hook.sh` — хук Claude Code на SessionStart/End, UserPromptSubmit, Stop,
|
||||||
Notification, SubagentStart/Stop, PostModelSwitch, PreToolUse(Agent|Task). Дописывает событие
|
Notification, SubagentStart/Stop, PostModelSwitch, PreToolUse(Agent|Task). Дописывает событие
|
||||||
в `~/.cache/claude-agents/<session_id>.jsonl` вместе с PID процесса `claude`,
|
в `~/.cache/claude-agents/<session_id>.jsonl` вместе с PID процесса `claude`,
|
||||||
`ITERM_SESSION_ID` или сокетом/панелью tmux.
|
`ITERM_SESSION_ID` или сокетом/панелью tmux.
|
||||||
- `files/claude_agents.py` — скрипт iTerm2 AutoLaunch, один файл:
|
- `files/claude_agents.py` — один файл, два режима (`run`):
|
||||||
- `refresher` раз в `poll_sec` перечитывает jsonl (`load_sessions` → `replay`), сверяет с живыми
|
- `--headless` (`serve`) — служба launchd `com.andrey.claude-agents`, единственный владелец порта:
|
||||||
сессиями iTerm2, процессами (`ps`) и панелями tmux, дочитывает из транскриптов модель и токены,
|
- `refresher` раз в `poll_sec` перечитывает jsonl (`load_sessions` → `replay`), сверяет с
|
||||||
из git — worktree; собирает `state["tree"]` (`build_tree`).
|
процессами (`ps`) и панелями tmux, дочитывает из транскриптов модель и токены, из git —
|
||||||
- `usage_poller` / `balance_poller` ходят в `api.anthropic.com/api/oauth/usage` и
|
worktree; собирает `state["tree"]` (`build_tree`, без данных iTerm2).
|
||||||
`.../prepaid/credits` (запасной путь — claude.ai с `sessionKey` из Keychain).
|
- `usage_poller` / `balance_poller` ходят в `api.anthropic.com/api/oauth/usage` и
|
||||||
- HTTP-сервер на `127.0.0.1:8766` (`handle`) отдаёт страницу, которую iTerm2 показывает как web view
|
`.../prepaid/credits` (запасной путь — claude.ai с `sessionKey` из Keychain).
|
||||||
tool: `/`, `/fragment` (дерево), `/limits`, `/message` (последние сообщения), `/activate`,
|
- HTTP-сервер (`handle`): `/`, `/fragment` (дерево), `/limits`, `/message` (последние
|
||||||
`/open-wt`, `/raw` (отладка).
|
сообщения), `/activate` и `/open-wt` (через tmux), `/raw` (отладка).
|
||||||
- `theme_poller` подстраивает цвета под профиль iTerm2.
|
- без флага (`register_toolbelt`) — скрипт AutoLaunch iTerm2: регистрирует web view «Claude» на
|
||||||
|
адрес службы; если порт молчит — `launchctl kickstart`. Свой сервер не поднимает.
|
||||||
|
- `files/launchagent.py` — пишет plist службы (`write <plist> <python> <script> <log>`).
|
||||||
- `files/settings_hooks.py` — добавляет/убирает хуки панели в `settings.json` (`add` / `remove`).
|
- `files/settings_hooks.py` — добавляет/убирает хуки панели в `settings.json` (`add` / `remove`).
|
||||||
- `files/config.example.json` — все ключи настроек; рабочий файл `~/.config/claude-agents/config.json`
|
- `files/config.example.json` — все ключи настроек; рабочий файл `~/.config/claude-agents/config.json`
|
||||||
перечитывается на лету, кроме `port`.
|
перечитывается на лету, кроме `port`.
|
||||||
- `install.sh` / `uninstall.sh [--purge]` — раскладывают файлы по местам (таблица в README.md).
|
- `install.sh` / `uninstall.sh [--purge]` — раскладывают файлы, ставят/убирают службу (таблица в README.md).
|
||||||
|
|
||||||
## Команды
|
## Команды
|
||||||
- `./install.sh` — установить или обновить у себя; затем Toolbelt → Claude и перезапуск сессий `claude`.
|
- `./install.sh` — установить или обновить у себя (за самим Mac); затем Toolbelt → Claude и перезапуск сессий `claude`.
|
||||||
- `./uninstall.sh` — удалить (`--purge` — вместе с настройками и логами).
|
- `./uninstall.sh` — удалить (`--purge` — вместе с настройками и логами).
|
||||||
|
- `python3 -m unittest discover -s tests -v` — весь набор тестов (stdlib, без pytest; `node` нужен
|
||||||
|
для теста клиентского JS). Зелёным считается только весь набор.
|
||||||
- `python3 -m py_compile files/*.py && sh -n files/agents_panel_hook.sh && bash -n install.sh && bash -n uninstall.sh` —
|
- `python3 -m py_compile files/*.py && sh -n files/agents_panel_hook.sh && bash -n install.sh && bash -n uninstall.sh` —
|
||||||
синтаксическая проверка; сборки нет.
|
синтаксическая проверка; сборки нет.
|
||||||
- `curl -s http://127.0.0.1:8766/raw` — что видит запущенная панель: логи хука, причины скрытия
|
- `curl -s http://127.0.0.1:8766/raw` — что видит запущенная панель: логи хука, причины скрытия
|
||||||
сессий, ошибки лимитов и баланса.
|
сессий, ошибки лимитов и баланса. Лог службы — `~/Library/Logs/claude-agents/panel.log`,
|
||||||
|
состояние — `launchctl print gui/$(id -u)/com.andrey.claude-agents`.
|
||||||
|
|
||||||
## Тесты
|
## Тесты
|
||||||
- Автотестов пока нет. `claude_agents.py` импортируется без iTerm2 (`iterm2 = None`), так что чистые
|
- `tests/` — `unittest`. `claude_agents.py` импортируется без iTerm2 (`iterm2 = None`). Сеть,
|
||||||
функции (`replay`, `build_tree`, `parse_limits`, `parse_balance`, `session_name`, `settings_hooks.add/remove`)
|
Keychain, launchd, `ps` и tmux в тестах подменяются; реальные `~/.claude` и `~/Library` не трогаются.
|
||||||
можно покрывать pytest без iTerm2. Сеть, Keychain, `ps` и tmux в тестах мокаются.
|
- `install.sh` и `uninstall.sh` в тестах запускаются только с `SSH_CONNECTION`/`SSH_TTY` (проверка
|
||||||
- Фикстуры — образцы jsonl хука и ответов API в `tests/fixtures/`.
|
отказа): без охраны они остановили бы настоящую панель на порту 8766.
|
||||||
|
|
||||||
## Где лежит код
|
## Где лежит код
|
||||||
- Код — в `files/`; хук require-plan защищает его через `.claude/code-paths`. `install.sh` и
|
- Код — в `files/`; хук require-plan защищает его через `.claude/code-paths`. `install.sh` и
|
||||||
|
|||||||
43
README.md
43
README.md
@ -1,10 +1,15 @@
|
|||||||
# Claude — панель для iTerm2 Toolbelt
|
# Claude — панель сессий Claude Code
|
||||||
|
|
||||||
Сверху — дерево активных сессий Claude Code и их субагентов (модель, контекст, worktree),
|
Сверху — дерево активных сессий Claude Code и их субагентов (модель, контекст, worktree),
|
||||||
снизу — лимиты подписки и баланс extra usage.
|
снизу — лимиты подписки и баланс extra usage. Панель — страница на `http://127.0.0.1:8766/`:
|
||||||
|
её показывает iTerm2 Toolbelt, её можно открыть в браузере, в том числе с iPad через SSH-туннель.
|
||||||
|
Сервер панели — фоновая служба macOS, iTerm2 для неё не нужен.
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
|
Запускать за самим Mac, не по SSH (из SSH-сессии launchd и iTerm2 не настроить —
|
||||||
|
установщик откажется):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tar xzf claude-panel.tar.gz
|
tar xzf claude-panel.tar.gz
|
||||||
cd claude-panel
|
cd claude-panel
|
||||||
@ -14,26 +19,40 @@ cd claude-panel
|
|||||||
Затем в iTerm2: **View → Toolbelt → Show Toolbelt**, **Toolbelt → Claude**, и перезапустите
|
Затем в iTerm2: **View → Toolbelt → Show Toolbelt**, **Toolbelt → Claude**, и перезапустите
|
||||||
открытые сессии `claude`. Повторный запуск `install.sh` обновляет установленное.
|
открытые сессии `claude`. Повторный запуск `install.sh` обновляет установленное.
|
||||||
|
|
||||||
Нужно: macOS, iTerm2, python3, jq (`brew install jq`, в macOS 15+ уже есть).
|
Нужно: macOS, python3, jq (`brew install jq`, в macOS 15+ уже есть); iTerm2 — для Toolbelt.
|
||||||
|
|
||||||
## Что ставится
|
## Что ставится
|
||||||
|
|
||||||
| Что | Куда |
|
| Что | Куда |
|
||||||
|---|---|
|
|---|---|
|
||||||
| скрипт панели | `~/Library/Application Support/iTerm2/Scripts/AutoLaunch/claude_agents.py` |
|
| служба (сервер панели) | `~/.local/share/claude-agents/claude_agents.py` |
|
||||||
|
| описание службы launchd | `~/Library/LaunchAgents/com.andrey.claude-agents.plist` |
|
||||||
|
| лог службы | `~/Library/Logs/claude-agents/panel.log` |
|
||||||
|
| скрипт Toolbelt | `~/Library/Application Support/iTerm2/Scripts/AutoLaunch/claude_agents.py` |
|
||||||
| хук Claude Code | `~/.claude/hooks/agents_panel_hook.sh` + записи в `~/.claude/settings.json` |
|
| хук Claude Code | `~/.claude/hooks/agents_panel_hook.sh` + записи в `~/.claude/settings.json` |
|
||||||
| пример настроек | `~/.config/claude-agents/config.example.json` |
|
| пример настроек | `~/.config/claude-agents/config.example.json` |
|
||||||
|
|
||||||
Перед изменением `settings.json` и заменой файлов старые копии кладутся в
|
Служба запускается при входе в систему и перезапускается при падении. Перед изменением
|
||||||
`~/.claude-panel-backup/<дата>/`. Ваши остальные хуки и настройки не трогаются.
|
`settings.json` и заменой файлов старые копии кладутся в `~/.claude-panel-backup/<дата>/`.
|
||||||
|
Ваши остальные хуки и настройки не трогаются.
|
||||||
|
|
||||||
|
## С iPad (Termius)
|
||||||
|
|
||||||
|
1. В Termius: **Port Forwarding → Local**: хост — ваш Mac, local port `8766`,
|
||||||
|
bind address `127.0.0.1`, destination `127.0.0.1`, destination port `8766`.
|
||||||
|
2. Запустите forward и откройте в Safari `http://127.0.0.1:8766` — именно `127.0.0.1`,
|
||||||
|
не `localhost` (сервер слушает только IPv4).
|
||||||
|
|
||||||
|
Сервер доступен только с самого Mac, в сеть не открывается: с других устройств — только через туннель.
|
||||||
|
Клики работают через tmux, так что переходы и `lf` появляются в том же tmux, что у вас в Termius.
|
||||||
|
|
||||||
## Работа с панелью
|
## Работа с панелью
|
||||||
|
|
||||||
- клик по сессии или агенту — последние сообщения (копятся, пока окно открыто);
|
- клик по сессии или агенту — последние сообщения (копятся, пока окно открыто);
|
||||||
- ⌘-клик по сессии — перейти в её вкладку;
|
- ⌘-клик по сессии — перейти в её панель tmux;
|
||||||
- клик по `⎇ папка` — открыть в ней `lf` в соседней панели;
|
- клик по `⎇ папка` — открыть в ней `lf` в соседней панели tmux;
|
||||||
- цвет точки: зелёный — работает, жёлтый — ждёт вас, серый — свободен;
|
- цвет точки: зелёный — работает, жёлтый — ждёт вас, серый — свободен;
|
||||||
- сессии внутри tmux (и `tmux -CC`) тоже видны: клик по `⎇` делит панель tmux, ⌘-клик переключает на неё;
|
- переходы и `lf` работают для сессий в tmux; сессии вне tmux только показываются;
|
||||||
- цвет токенов: жёлтый > 500k, красный > 700k.
|
- цвет токенов: жёлтый > 500k, красный > 700k.
|
||||||
|
|
||||||
## Настройки
|
## Настройки
|
||||||
@ -43,8 +62,10 @@ cd claude-panel
|
|||||||
|
|
||||||
## Если что-то не так
|
## Если что-то не так
|
||||||
|
|
||||||
`http://127.0.0.1:8766/raw` — что панель видит: логи хука, причины скрытия сессий,
|
- `http://127.0.0.1:8766/raw` — что панель видит: логи хука, причины скрытия сессий,
|
||||||
ошибки лимитов и баланса.
|
ошибки лимитов и баланса;
|
||||||
|
- `~/Library/Logs/claude-agents/panel.log` — лог службы;
|
||||||
|
- `launchctl print gui/$(id -u)/com.andrey.claude-agents` — состояние службы.
|
||||||
|
|
||||||
## Удаление
|
## Удаление
|
||||||
|
|
||||||
|
|||||||
72
install.sh
72
install.sh
@ -1,8 +1,18 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Установка панели «Claude» для iTerm2 Toolbelt: дерево сессий и агентов Claude Code,
|
# Установка панели «Claude»: дерево сессий и агентов Claude Code, лимиты подписки и баланс.
|
||||||
# лимиты подписки и баланс. Повторный запуск безопасен — обновляет установленное.
|
# Сервер панели — служба launchd (работает и без iTerm2), iTerm2 показывает её в Toolbelt.
|
||||||
|
# Повторный запуск безопасен — обновляет установленное.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
say() { printf '\033[1m==>\033[0m %s\n' "$*"; }
|
||||||
|
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
||||||
|
warn() { printf ' \033[33m!\033[0m %s\n' "$*"; }
|
||||||
|
die() { printf '\033[31mОшибка:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# из SSH-сессии launchd (gui/<uid>) и iTerm2 не настроить, а прежний сервер уже был бы остановлен
|
||||||
|
[ -z "${SSH_CONNECTION:-}${SSH_TTY:-}" ] \
|
||||||
|
|| die "запустите за самим Mac, не по SSH — из SSH-сессии launchd и iTerm2 не настроить"
|
||||||
|
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
FILES="$HERE/files"
|
FILES="$HERE/files"
|
||||||
AUTOLAUNCH="$HOME/Library/Application Support/iTerm2/Scripts/AutoLaunch"
|
AUTOLAUNCH="$HOME/Library/Application Support/iTerm2/Scripts/AutoLaunch"
|
||||||
@ -11,11 +21,12 @@ SETTINGS="$HOME/.claude/settings.json"
|
|||||||
CONFIG_DIR="$HOME/.config/claude-agents"
|
CONFIG_DIR="$HOME/.config/claude-agents"
|
||||||
BACKUP_DIR="$HOME/.claude-panel-backup/$(date +%Y%m%d-%H%M%S)-$$"
|
BACKUP_DIR="$HOME/.claude-panel-backup/$(date +%Y%m%d-%H%M%S)-$$"
|
||||||
PORT=8766
|
PORT=8766
|
||||||
|
LABEL=com.andrey.claude-agents
|
||||||
|
SERVICE_DIR="$HOME/.local/share/claude-agents"
|
||||||
|
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
||||||
|
SERVICE_LOG="$HOME/Library/Logs/claude-agents/panel.log"
|
||||||
|
GUI="gui/$(id -u)"
|
||||||
|
|
||||||
say() { printf '\033[1m==>\033[0m %s\n' "$*"; }
|
|
||||||
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
|
||||||
warn() { printf ' \033[33m!\033[0m %s\n' "$*"; }
|
|
||||||
die() { printf '\033[31mОшибка:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
||||||
backup() { mkdir -p "$BACKUP_DIR"; cp -p "$1" "$BACKUP_DIR/"; }
|
backup() { mkdir -p "$BACKUP_DIR"; cp -p "$1" "$BACKUP_DIR/"; }
|
||||||
|
|
||||||
# ---------- проверки ----------
|
# ---------- проверки ----------
|
||||||
@ -23,11 +34,12 @@ say "Проверяю окружение"
|
|||||||
[ "$(uname)" = "Darwin" ] || [ "${CLAUDE_PANEL_ANY_OS:-}" = 1 ] || die "нужен macOS с iTerm2"
|
[ "$(uname)" = "Darwin" ] || [ "${CLAUDE_PANEL_ANY_OS:-}" = 1 ] || die "нужен macOS с iTerm2"
|
||||||
python3 -c 'import sys; assert sys.version_info >= (3, 8)' 2>/dev/null \
|
python3 -c 'import sys; assert sys.version_info >= (3, 8)' 2>/dev/null \
|
||||||
|| die "нужен python3 ≥ 3.8 (xcode-select --install или brew install python)"
|
|| die "нужен python3 ≥ 3.8 (xcode-select --install или brew install python)"
|
||||||
ok "python3: $(python3 -V 2>&1)"
|
PY="$(python3 -c 'import sys; print(sys.executable)')"
|
||||||
|
ok "python3: $(python3 -V 2>&1) ($PY)"
|
||||||
if command -v jq >/dev/null; then ok "jq: $(command -v jq)"
|
if command -v jq >/dev/null; then ok "jq: $(command -v jq)"
|
||||||
else warn "нет jq — хук без него молча ничего не пишет. Установите: brew install jq"; fi
|
else warn "нет jq — хук без него молча ничего не пишет. Установите: brew install jq"; fi
|
||||||
if [ -d "/Applications/iTerm.app" ] || [ -d "$HOME/Applications/iTerm.app" ]; then ok "iTerm2 найден"
|
if [ -d "/Applications/iTerm.app" ] || [ -d "$HOME/Applications/iTerm.app" ]; then ok "iTerm2 найден"
|
||||||
else warn "iTerm2 не найден в /Applications — панель будет работать только в нём"; fi
|
else warn "iTerm2 не найден в /Applications — панель будет доступна только в браузере"; fi
|
||||||
|
|
||||||
# ---------- хук Claude Code ----------
|
# ---------- хук Claude Code ----------
|
||||||
say "Хук Claude Code"
|
say "Хук Claude Code"
|
||||||
@ -41,7 +53,7 @@ if [ "$added" = "—" ]; then ok "settings.json: хуки уже на месте
|
|||||||
else ok "settings.json: добавлены хуки $added (копия старого — в $BACKUP_DIR)"; fi
|
else ok "settings.json: добавлены хуки $added (копия старого — в $BACKUP_DIR)"; fi
|
||||||
|
|
||||||
# ---------- скрипт iTerm2 ----------
|
# ---------- скрипт iTerm2 ----------
|
||||||
say "Скрипт iTerm2"
|
say "Скрипт iTerm2 (регистрация в Toolbelt)"
|
||||||
mkdir -p "$AUTOLAUNCH"
|
mkdir -p "$AUTOLAUNCH"
|
||||||
if [ -f "$AUTOLAUNCH/claude_usage.py" ]; then # прежняя отдельная панель лимитов
|
if [ -f "$AUTOLAUNCH/claude_usage.py" ]; then # прежняя отдельная панель лимитов
|
||||||
backup "$AUTOLAUNCH/claude_usage.py" && rm "$AUTOLAUNCH/claude_usage.py"
|
backup "$AUTOLAUNCH/claude_usage.py" && rm "$AUTOLAUNCH/claude_usage.py"
|
||||||
@ -57,6 +69,27 @@ install -m 644 "$FILES/config.example.json" "$CONFIG_DIR/config.example.json"
|
|||||||
if [ -f "$CONFIG_DIR/config.json" ]; then ok "ваш config.json не тронут"
|
if [ -f "$CONFIG_DIR/config.json" ]; then ok "ваш config.json не тронут"
|
||||||
else ok "пример настроек: $CONFIG_DIR/config.example.json (скопируйте в config.json, чтобы менять)"; fi
|
else ok "пример настроек: $CONFIG_DIR/config.example.json (скопируйте в config.json, чтобы менять)"; fi
|
||||||
|
|
||||||
|
# ---------- фоновая служба ----------
|
||||||
|
say "Фоновая служба"
|
||||||
|
mkdir -p "$SERVICE_DIR"
|
||||||
|
install -m 644 "$FILES/claude_agents.py" "$SERVICE_DIR/claude_agents.py"
|
||||||
|
python3 "$FILES/launchagent.py" write "$PLIST" "$PY" "$SERVICE_DIR/claude_agents.py" "$SERVICE_LOG" >/dev/null
|
||||||
|
ok "$PLIST"
|
||||||
|
launchctl bootout "$GUI/$LABEL" >/dev/null 2>&1 || true
|
||||||
|
# прежняя копия панели (сервер внутри iTerm2) держит порт — останавливаем, чтобы его заняла служба
|
||||||
|
if command -v lsof >/dev/null; then
|
||||||
|
pids=$(lsof -ti "tcp:$PORT" -sTCP:LISTEN 2>/dev/null || true)
|
||||||
|
if [ -n "$pids" ]; then kill $pids 2>/dev/null || true; sleep 1; ok "остановлена прежняя версия панели"; fi
|
||||||
|
fi
|
||||||
|
launchctl bootstrap "$GUI" "$PLIST" || die "launchd не принял службу — смотрите $SERVICE_LOG"
|
||||||
|
up=0
|
||||||
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
if curl -s -o /dev/null --max-time 1 "http://127.0.0.1:$PORT/"; then up=1; break; fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ $up = 1 ]; then ok "служба запущена: http://127.0.0.1:$PORT/"
|
||||||
|
else warn "служба не ответила — смотрите $SERVICE_LOG"; fi
|
||||||
|
|
||||||
# ---------- Python API в iTerm2 ----------
|
# ---------- Python API в iTerm2 ----------
|
||||||
say "iTerm2"
|
say "iTerm2"
|
||||||
iterm_running=0
|
iterm_running=0
|
||||||
@ -74,28 +107,23 @@ else
|
|||||||
ok "Python API уже включён"
|
ok "Python API уже включён"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# старая копия панели держит порт — останавливаем, чтобы запустилась новая
|
|
||||||
if command -v lsof >/dev/null; then
|
|
||||||
pids=$(lsof -ti "tcp:$PORT" -sTCP:LISTEN 2>/dev/null || true)
|
|
||||||
if [ -n "$pids" ]; then kill $pids 2>/dev/null || true; sleep 1; ok "остановлена прежняя версия панели"; fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $iterm_running = 1 ] && [ "$api" = 1 ]; then
|
if [ $iterm_running = 1 ] && [ "$api" = 1 ]; then
|
||||||
if osascript -e 'tell application "iTerm2" to launch API script named "claude_agents"' >/dev/null 2>&1; then
|
if osascript -e 'tell application "iTerm2" to launch API script named "claude_agents"' >/dev/null 2>&1; then
|
||||||
ok "панель запущена"
|
ok "панель зарегистрирована в Toolbelt"
|
||||||
else
|
else
|
||||||
warn "не смог запустить сам — Scripts → AutoLaunch → claude_agents.py"
|
warn "не смог зарегистрировать сам — Scripts → AutoLaunch → claude_agents.py"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
warn "панель запустится сама при следующем запуске iTerm2"
|
warn "в Toolbelt панель появится при следующем запуске iTerm2"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
||||||
Готово. Осталось:
|
Готово. Панель работает и без iTerm2: http://127.0.0.1:$PORT/
|
||||||
1. В iTerm2: View → Toolbelt → Show Toolbelt, затем Toolbelt → Claude.
|
(с iPad — через SSH-туннель на этот адрес, см. README).
|
||||||
2. Перезапустите открытые сессии claude — хуки подхватываются при старте сессии.
|
В iTerm2: View → Toolbelt → Show Toolbelt, затем Toolbelt → Claude.
|
||||||
|
Перезапустите открытые сессии claude — хуки подхватываются при старте сессии.
|
||||||
|
|
||||||
Проверка, что всё работает: http://127.0.0.1:$PORT/raw
|
Проверка: http://127.0.0.1:$PORT/raw · лог службы: $SERVICE_LOG
|
||||||
Удаление: $HERE/uninstall.sh
|
Удаление: $HERE/uninstall.sh
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
32
tests/test_install.py
Normal file
32
tests/test_install.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
|
||||||
|
SSH_ENVS = ({"SSH_CONNECTION": "10.0.0.2 50000 10.0.0.1 22"}, {"SSH_TTY": "/dev/ttys009"})
|
||||||
|
|
||||||
|
|
||||||
|
class RefuseOverSsh(unittest.TestCase):
|
||||||
|
"""Только охранная проверка: реальная установка в этих тестах не запускается."""
|
||||||
|
|
||||||
|
def assert_refused(self, *cmd):
|
||||||
|
for extra in SSH_ENVS:
|
||||||
|
with self.subTest(cmd=cmd, env=extra), tempfile.TemporaryDirectory() as home:
|
||||||
|
env = {k: v for k, v in os.environ.items() if k not in ("SSH_CONNECTION", "SSH_TTY")}
|
||||||
|
env.update(HOME=home, **extra)
|
||||||
|
r = subprocess.run(["bash", *cmd], cwd=ROOT, env=env, capture_output=True, text=True)
|
||||||
|
self.assertNotEqual(r.returncode, 0)
|
||||||
|
self.assertIn("за самим Mac", r.stdout + r.stderr)
|
||||||
|
self.assertEqual(os.listdir(home), [])
|
||||||
|
|
||||||
|
def test_install_refuses_over_ssh(self):
|
||||||
|
self.assert_refused("install.sh")
|
||||||
|
|
||||||
|
def test_uninstall_refuses_over_ssh(self):
|
||||||
|
self.assert_refused("uninstall.sh")
|
||||||
|
self.assert_refused("uninstall.sh", "--purge")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
20
uninstall.sh
20
uninstall.sh
@ -1,14 +1,26 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Удаление панели «Claude». С --purge удаляет ещё настройки и логи хука.
|
# Удаление панели «Claude». С --purge удаляет ещё настройки и логи.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
||||||
|
die() { printf '\033[31mОшибка:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
[ -z "${SSH_CONNECTION:-}${SSH_TTY:-}" ] \
|
||||||
|
|| die "запустите за самим Mac, не по SSH — из SSH-сессии launchd и iTerm2 не настроить"
|
||||||
|
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
AUTOLAUNCH="$HOME/Library/Application Support/iTerm2/Scripts/AutoLaunch"
|
AUTOLAUNCH="$HOME/Library/Application Support/iTerm2/Scripts/AutoLaunch"
|
||||||
SETTINGS="$HOME/.claude/settings.json"
|
SETTINGS="$HOME/.claude/settings.json"
|
||||||
BACKUP_DIR="$HOME/.claude-panel-backup/$(date +%Y%m%d-%H%M%S)-$$-uninstall"
|
BACKUP_DIR="$HOME/.claude-panel-backup/$(date +%Y%m%d-%H%M%S)-$$-uninstall"
|
||||||
PORT=8766
|
PORT=8766
|
||||||
|
LABEL=com.andrey.claude-agents
|
||||||
|
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
||||||
|
|
||||||
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
# служба — первой, иначе launchd поднимет сервер снова
|
||||||
|
launchctl bootout "gui/$(id -u)/$LABEL" >/dev/null 2>&1 && ok "служба остановлена" || true
|
||||||
|
rm -f "$PLIST"
|
||||||
|
rm -rf "$HOME/.local/share/claude-agents"
|
||||||
|
ok "служба удалена"
|
||||||
|
|
||||||
if command -v lsof >/dev/null; then
|
if command -v lsof >/dev/null; then
|
||||||
pids=$(lsof -ti "tcp:$PORT" -sTCP:LISTEN 2>/dev/null || true)
|
pids=$(lsof -ti "tcp:$PORT" -sTCP:LISTEN 2>/dev/null || true)
|
||||||
@ -25,9 +37,9 @@ fi
|
|||||||
rm -f "$HOME/.claude/hooks/agents_panel_hook.sh" && ok "хук удалён"
|
rm -f "$HOME/.claude/hooks/agents_panel_hook.sh" && ok "хук удалён"
|
||||||
|
|
||||||
if [ "${1:-}" = "--purge" ]; then
|
if [ "${1:-}" = "--purge" ]; then
|
||||||
rm -rf "$HOME/.config/claude-agents" "$HOME/.cache/claude-agents"
|
rm -rf "$HOME/.config/claude-agents" "$HOME/.cache/claude-agents" "$HOME/Library/Logs/claude-agents"
|
||||||
ok "настройки и логи удалены"
|
ok "настройки и логи удалены"
|
||||||
else
|
else
|
||||||
echo " Настройки (~/.config/claude-agents) и логи (~/.cache/claude-agents) оставлены; --purge удалит и их."
|
echo " Настройки (~/.config/claude-agents) и логи (~/.cache/claude-agents, ~/Library/Logs/claude-agents) оставлены; --purge удалит и их."
|
||||||
fi
|
fi
|
||||||
echo "Панель «Claude» исчезнет из меню Toolbelt после перезапуска iTerm2."
|
echo "Панель «Claude» исчезнет из меню Toolbelt после перезапуска iTerm2."
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user