Complete rewrite from tmux + JSONL parsing to a clean PTY-based approach. The proxy spawns Claude CLI in a pseudo-terminal via node-pty and relays terminal I/O as binary WebSocket frames to the simple-chat backend, which forwards them to the browser where xterm.js renders a full terminal. - node-pty PTY manager with 50KB replay buffer per session - Binary frame protocol with chatId multiplexing - WebSocket client with auto-reconnection and heartbeat - Directory listing and session listing for the web UI - README with setup instructions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
262 lines
17 KiB
Markdown
262 lines
17 KiB
Markdown
# Claude CLI Proxy — Архитектура
|
||
|
||
## Обзор
|
||
|
||
claude-cli-proxy — это локальное приложение на Node.js, которое соединяет Claude Code CLI с веб-интерфейсом simple-chat. Оно подключается к бэкенду simple-chat через исходящее WebSocket-соединение, управляет tmux-сессиями для запуска экземпляров Claude CLI и отслеживает их вывод через JSONL-файлы транскриптов и захват терминала.
|
||
|
||
## Диаграмма компонентов
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Mac разработчика │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||
│ │ claude-cli-proxy │ │
|
||
│ │ │ │
|
||
│ │ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
|
||
│ │ │ WsClient │ │ TmuxManager │ │SessionMonitor│ │ │
|
||
│ │ │(connection/)│ │ (tmux/) │ │ (monitor/) │ │ │
|
||
│ │ └──────┬──────┘ └──────┬───────┘ └──────┬───────┘ │ │
|
||
│ │ │ │ │ │ │
|
||
│ │ │ ┌──────┴───────┐ ┌──────┴───────┐ │ │
|
||
│ │ │ │ tmux-сессия │ │ JSONL-файлы │ │ │
|
||
│ │ │ │"claude-proxy"│ │ ~/.claude/ │ │ │
|
||
│ │ │ │ ├─ окно 1 │ │ projects/... │ │ │
|
||
│ │ │ │ ├─ окно 2 │ └──────────────┘ │ │
|
||
│ │ │ │ └─ ... │ │ │
|
||
│ │ │ └──────────────┘ │ │
|
||
│ └─────────┼────────────────────────────────────────────────┘ │
|
||
│ │ WebSocket │
|
||
└────────────┼────────────────────────────────────────────────────┘
|
||
│
|
||
┌────────┴────────┐
|
||
│ simple-chat │
|
||
│ бэкенд │
|
||
│ (модуль │
|
||
│ agent-proxy) │
|
||
└─────────────────┘
|
||
```
|
||
|
||
## Компоненты
|
||
|
||
### 1. Точка входа (`main.ts`)
|
||
|
||
Класс `ClaudeCliProxy` оркестрирует запуск:
|
||
1. Загрузка конфигурации из `.env`
|
||
2. Установка/обновление хука SessionStart для Claude CLI
|
||
3. Подключение к бэкенду через WebSocket
|
||
4. Инициализация tmux-сессии
|
||
5. Запуск мониторинга активных окон
|
||
|
||
### 2. WebSocket-клиент (`connection/ws-client.ts`)
|
||
|
||
- Подключается к `SERVER_URL` с `PROXY_TOKEN` для аутентификации
|
||
- Обрабатывает входящие команды от бэкенда
|
||
- Отправляет события (сообщения, обновления статуса, скриншоты) на бэкенд
|
||
- Переподключается с экспоненциальной задержкой при разрыве соединения
|
||
|
||
### 3. Протокол (`connection/protocol.ts`)
|
||
|
||
Общие определения типов сообщений для WebSocket-протокола.
|
||
|
||
#### Сообщения от бэкенда к прокси
|
||
|
||
| Сообщение | Описание |
|
||
|-----------|----------|
|
||
| `sync_state` | Отправить текущее состояние всех окон при подключении |
|
||
| `create_window` | Создать новое tmux-окно, запустить `claude` или `claude --resume <id>` |
|
||
| `send_message` | Отправить текстовый ввод в Claude CLI в указанном окне |
|
||
| `send_command` | Отправить произвольную команду в tmux-панель |
|
||
| `send_command_capture` | Отправить команду и захватить вывод |
|
||
| `send_key` | Отправить нажатие клавиши (например, Enter, Escape, Tab) |
|
||
| `kill_window` | Закрыть tmux-окно |
|
||
| `request_screenshot` | Захватить текущее содержимое терминала |
|
||
| `list_directories` | Получить список доступных рабочих директорий |
|
||
| `list_sessions` | Получить список возобновляемых сессий Claude |
|
||
|
||
#### Сообщения от прокси к бэкенду
|
||
|
||
| Сообщение | Описание |
|
||
|-----------|----------|
|
||
| `window_ready` | Новое окно создано, сообщается windowId |
|
||
| `message` | Новое сообщение ассистента/пользователя из JSONL-транскрипта |
|
||
| `status` | Изменение статуса Claude CLI (размышление, использование инструмента, простой) |
|
||
| `interactive` | Обнаружен интерактивный UI (запрос разрешения и т.д.) |
|
||
| `status_line` | Разобранные данные строки состояния (модель, контекст %, сессия %) |
|
||
| `screenshot` | Скриншот терминала (захваченное содержимое панели) |
|
||
| `window_closed` | Окно было закрыто или процесс завершился |
|
||
| `directories` | Ответ на list_directories |
|
||
| `sessions_list` | Ответ на list_sessions |
|
||
| `command_output` | Ответ на send_command_capture |
|
||
| `error` | Отчёт об ошибке |
|
||
|
||
### 4. Менеджер Tmux (`tmux/manager.ts`)
|
||
|
||
Управляет tmux-сессией `claude-proxy` (настраивается через `TMUX_SESSION`).
|
||
|
||
Операции:
|
||
- **Создание окна:** `tmux new-window` с командой `claude` или `claude --resume <sessionId>`
|
||
- **Удаление окна:** `tmux kill-window -t <windowId>`
|
||
- **Отправка клавиш:** `tmux send-keys -t <windowId>` для пользовательского ввода
|
||
- **Захват панели:** `tmux capture-pane -t <windowId> -p` для скриншотов и строки состояния
|
||
|
||
Каждый чат в simple-chat соответствует одному tmux-окну. Идентификатор окна хранится в поле `agentWindowId` чата.
|
||
|
||
### 5. Монитор сессий (`monitor/session-monitor.ts`)
|
||
|
||
Оркестратор, запускающий два цикла опроса для каждого активного окна:
|
||
|
||
#### Опрос JSONL (интервал 2 секунды)
|
||
|
||
1. Определение пути к JSONL-файлу по sessionId: `~/.claude/projects/{encoded_cwd}/{sessionId}.jsonl`
|
||
2. Чтение файла с последнего известного смещения в байтах (хранится в `monitor-state.ts`)
|
||
3. Парсинг новых JSONL-записей с помощью `jsonl-parser.ts`
|
||
4. Фильтрация системных/внутренних сообщений
|
||
5. Отправка релевантных сообщений на бэкенд через WebSocket
|
||
|
||
#### Опрос терминала (интервал 1 секунда)
|
||
|
||
1. Захват последних 3 строк tmux-панели (область строки состояния)
|
||
2. Парсинг с помощью `terminal-parser.ts` для извлечения:
|
||
- **Строка 1:** Название проекта, использование сессии %, использование за неделю %
|
||
- **Строка 2:** Название модели, использование контекста %
|
||
- **Строка 3:** Индикатор режима разрешений
|
||
3. Обнаружение интерактивных состояний UI (запросы разрешений, запросы ввода)
|
||
4. Отправка событий `status_line`, `status` и `interactive` на бэкенд
|
||
|
||
### 6. JSONL-парсер (`monitor/jsonl-parser.ts`)
|
||
|
||
Разбирает файлы транскриптов Claude CLI в формате JSONL. Каждая строка — это JSON-объект, представляющий событие разговора. Парсер:
|
||
- Читает новые байты с последнего смещения
|
||
- Разбирает каждую строку как JSON
|
||
- Фильтрует системные сообщения (инициализация, конфигурация и т.д.)
|
||
- Извлекает сообщения пользователя, сообщения ассистента, вызовы инструментов и результаты инструментов
|
||
|
||
### 7. Парсер терминала (`monitor/terminal-parser.ts`)
|
||
|
||
Разбирает строку состояния Claude CLI, отображаемую в нижней части терминала:
|
||
|
||
```
|
||
Строка 1: agent_dev │ session: 24% │ week: 3%
|
||
Строка 2: Opus 4.6 (1M context) │ Ctx: 2%
|
||
Строка 3: ⏵⏵ bypass permissions on (shift+tab to cycle)
|
||
```
|
||
|
||
Извлекает структурированные данные: название проекта, модель, процент контекста, использование сессии/недели, режим разрешений.
|
||
|
||
Также обнаруживает интерактивные состояния UI, когда Claude ожидает ввода пользователя (запросы разрешений, вопросы да/нет).
|
||
|
||
### 8. Состояние монитора (`monitor/monitor-state.ts`)
|
||
|
||
Отслеживает состояние мониторинга для каждого окна:
|
||
- **Смещение в байтах:** Последняя позиция чтения в JSONL-файле (для инкрементального чтения)
|
||
- **Кеш mtime:** Время модификации файла (пропуск повторного чтения неизменённых файлов)
|
||
|
||
### 9. Система хуков (`hook/`)
|
||
|
||
#### hook.ts
|
||
|
||
Управляет хуком SessionStart для Claude CLI:
|
||
- Устанавливает/обновляет запись хука в `~/.claude/settings.json`
|
||
- Предоставляет CRUD-операции для `~/.claude-proxy/session_map.json`
|
||
|
||
#### session-start-hook.ts
|
||
|
||
Скрипт, который запускается при старте новой сессии Claude CLI. Он:
|
||
1. Получает sessionId от Claude CLI
|
||
2. Определяет текущий идентификатор tmux-окна
|
||
3. Записывает маппинг `{windowId: sessionId}` в `~/.claude-proxy/session_map.json`
|
||
|
||
Этот маппинг необходим, потому что путь к JSONL-файлу зависит от sessionId, который становится известен только после запуска Claude CLI.
|
||
|
||
### 10. Карта сессий
|
||
|
||
Хранится в `~/.claude-proxy/session_map.json`.
|
||
|
||
Сопоставляет идентификаторы tmux-окон с идентификаторами сессий Claude CLI:
|
||
```json
|
||
{
|
||
"3": "abc123-def456",
|
||
"5": "789ghi-012jkl"
|
||
}
|
||
```
|
||
|
||
Используется монитором для нахождения правильного JSONL-файла транскрипта для каждого окна.
|
||
|
||
### 11. Загрузчик файлов (`files/downloader.ts`)
|
||
|
||
Скачивает файлы по URL из MinIO, на которые есть ссылки в сообщениях. Используется, когда вывод Claude CLI содержит ссылки на файлы, которые должны быть доступны через веб-интерфейс.
|
||
|
||
## Потоки данных
|
||
|
||
### Поток создания нового чата
|
||
|
||
1. Пользователь создаёт чат в веб-интерфейсе simple-chat
|
||
2. Бэкенд отправляет `create_window` прокси через WebSocket
|
||
3. Прокси создаёт tmux-окно, запускает `claude` (или `claude --resume <id>`)
|
||
4. Claude CLI запускается, срабатывает хук SessionStart
|
||
5. Хук записывает маппинг sessionId -> windowId в session_map.json
|
||
6. Прокси читает session_map, начинает мониторинг JSONL-файла и терминала
|
||
7. Прокси отправляет `window_ready` на бэкенд с windowId
|
||
|
||
### Поток сообщений
|
||
|
||
1. Пользователь вводит сообщение в simple-chat
|
||
2. Бэкенд отправляет `send_message` прокси
|
||
3. Прокси отправляет нажатия клавиш в tmux-окно через `tmux send-keys`
|
||
4. Claude CLI обрабатывает сообщение, записывает в JSONL-транскрипт
|
||
5. Опросчик JSONL обнаруживает новые байты, разбирает сообщения
|
||
6. Прокси отправляет события `message` на бэкенд
|
||
7. Бэкенд передаёт их на фронтенд через SSE
|
||
|
||
### Поток строки состояния
|
||
|
||
1. Опросчик терминала захватывает последние 3 строки tmux-панели каждую секунду
|
||
2. Парсер извлекает модель, контекст %, статистику использования
|
||
3. Прокси отправляет `status_line` на бэкенд
|
||
4. Фронтенд отображает строку состояния в реальном времени в UI чата
|
||
|
||
## Кодирование пути JSONL
|
||
|
||
Claude CLI хранит транскрипты по пути:
|
||
```
|
||
~/.claude/projects/{encoded_cwd}/{sessionId}.jsonl
|
||
```
|
||
|
||
Кодирование пути: символы `/` и `_` в пути рабочей директории заменяются на `-`.
|
||
|
||
Пример:
|
||
- Рабочая директория: `/Users/vigdorov/agent_dev`
|
||
- Закодированный путь: `-Users-vigdorov-agent-dev`
|
||
- Полный путь: `~/.claude/projects/-Users-vigdorov-agent-dev/{sessionId}.jsonl`
|
||
|
||
## Логика переподключения
|
||
|
||
WebSocket-клиент реализует экспоненциальную задержку:
|
||
1. При разрыве соединения ожидание `baseDelay` (начиная с ~1 секунды)
|
||
2. Каждая неудачная попытка переподключения удваивает задержку
|
||
3. Ограничение максимальной задержкой
|
||
4. При успешном переподключении отправляется `sync_state` с текущим состоянием окон
|
||
5. Сброс таймера задержки при успешном подключении
|
||
|
||
## Интеграция с simple-chat
|
||
|
||
### Бэкенд
|
||
|
||
- Модуль: `agent-proxy`
|
||
- WebSocket gateway обрабатывает подключения прокси
|
||
- REST API для управления agent-устройствами
|
||
- SSE relay передаёт обновления в реальном времени на фронтенд
|
||
|
||
### Фронтенд
|
||
|
||
- Маршрут: `/agent`
|
||
- `AgentPage` — список устройств/чатов
|
||
- `AgentChatPage` — вид чата с сообщениями в реальном времени и строкой состояния
|
||
- Slash-команды: `/model` (выбор модели), `/resume` (выбор сессии), `/clear`, `/compact`, `/plan`
|
||
|
||
### База данных
|
||
|
||
- Таблица `agent_devices` — зарегистрированные прокси-устройства
|
||
- Поля таблицы `chats`: `agentWindowId`, `agentWorkDir`, `agentSessionId`
|