feat: xterm.js + node-pty architecture for Claude CLI web terminal

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>
This commit is contained in:
2026-03-22 00:24:29 +03:00
commit 4a91896732
17 changed files with 6146 additions and 0 deletions

261
ARCHITECTURE.md Normal file
View File

@ -0,0 +1,261 @@
# 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`