[2026-06-26] taiga-vault: personal/projects/cursor-to-zulip/analysis.md

This commit is contained in:
Taiga
2026-06-26 06:45:24 +00:00
parent cb5a1389f9
commit eeaffce165
@@ -0,0 +1,184 @@
# CursorRemote → форк для Zulip
**Источник:** Анализ кода CursorRemote (v0.1.46) из `~/Developer/cursorremote/`
## Архитектура CursorRemote
```
Cursor IDE ←──CDP──→ Relay Server ←──socket.io──→ Browser Client
(Node.js) ←──Bot API───→ Telegram
```
Relay Server построен на EventEmitter. Основные компоненты:
### Data Flow (наблюдение)
1. **CDPBridge** (`cdp-bridge.ts`) — подключается к Cursor через CDP WebSocket (`--remote-debugging-port=9222`), управляет жизненным циклом соединения
2. **DOMExtractor** (`dom-extractor.ts`) — через `Runtime.evaluate` дёргает DOM Cursor внутри его renderer'а, вытаскивает `[data-flat-index]` элементы, парсит их в `ChatElement[]`
3. **WindowMonitor** — циклически опрашивает все окна Cursor, собирает `WindowSnapshot`
4. **StateManager** (`state-manager.ts`) — диффает новый state с предыдущим, эмитит `state:patch`
5. **Транспорты** подписываются на события StateManager
### Transport Architecture (ключевое для нас)
Контракт транспорта (`transports/types.ts`):
```typescript
export interface Transport {
readonly name: string;
start(): Promise<void>;
stop(): Promise<void>;
}
```
**TelegramTransport** (`transports/telegram/base.ts`):
- Наследуется от `BaseTelegramTransport` (абстрактный класс)
- Подписывается на `windowMonitor.on('window:update')` и `stateManager.on('state:patch')`
- В `processWindow()` получает `WindowSnapshot` → находит/создаёт topic (thread_id) → отправляет diff в Telegram
- В `onStatePatch()` отдельно обрабатывает `pendingApprovals` и `questionnaire` (они не привязаны к конкретному окну)
- Использует `formatter.ts` для конвертации `ChatElement` в HTML
### Что нам нужно реплицировать
Для Zulip не нужны:
- Интерактивные кнопки (approve/reject в Zulip — это реакция на пост, не инлайн-кнопки)
- Переключение вкладок/мод/моделей
- Typing indicator
Нужно:
- Дублировать сообщения (`HumanMessage`, `AssistantMessage`, `ToolCallElement`, `PlanBlock`, `RunCommand`)
- Публиковать в Zulip канал `#cursor` в отдельный топик (например `{windowTitle}/{tabTitle}`)
- Апдейтить существующие сообщения (через `edit_message` в Zulip API)
- Сообщать о новых approval-запросах
### Точки интеграции в код
| Файл | Что менять |
|---|---|
| `config.ts` | Добавить ZulipConfig (url, email, api_key, stream) |
| `index.ts` | Создавать ZulipTransport если включён в конфиге |
| `transports/zulip/` | Новая папка — весь транспорт |
| `transports/zulip/zulip-api.ts` | REST клиент для Zulip API |
| `transports/zulip/message-tracker.ts` | Отслеживание message_id (можно переиспользовать или скопировать) |
| `transports/telegram/formatter.ts` | Можно частично переиспользовать форматирование |
## Детали реализации (из code review)
### Конфигурация: что добавить в `config.ts`
Текущий конфиг — только telegram:
```typescript
telegram: {
enabled: process.env.TELEGRAM_ENABLED === 'true',
botToken: process.env.TELEGRAM_BOT_TOKEN ?? '',
preRegisteredUsers,
impl: (process.env.TELEGRAM_IMPL === 'raw' ? 'raw' : 'grammy') as 'grammy' | 'raw',
},
```
Нужно добавить:
```typescript
zulip: {
enabled: process.env.ZULIP_ENABLED === 'true',
url: process.env.ZULIP_URL ?? 'https://zulip.qentra.top',
email: process.env.ZULIP_BOT_EMAIL ?? '',
apiKey: process.env.ZULIP_BOT_API_KEY ?? '',
stream: process.env.ZULIP_STREAM ?? 'cursor',
}
```
### Архитектура транспорта
ZulipTransport должен **не наследовать** от BaseTelegramTransport (слишком много Telegram-специфики: форум топики, инлайн кнопки, typing indicators, activity messages). Правильнее — реализовать `Transport` интерфейс напрямую:
```typescript
class ZulipTransport implements Transport {
readonly name = 'zulip';
constructor(
config: ZulipConfig,
windowMonitor: WindowMonitor,
stateManager: StateManager,
commandExecutor: CommandExecutor,
cdpBridge: CDPBridge,
) {}
async start(): Promise<void> {
// 1. Подписаться на windowMonitor.on('window:update')
// 2. Подписаться на stateManager.on('state:patch')
}
async stop(): Promise<void> {
// Отписаться
}
}
```
### Детали WindowSnapshot (ключевые поля)
| Поле | Тип | Для Zulip |
|---|---|---|
| `windowTitle` | `string` | Часть топика: `windowTitle/tabTitle` |
| `chatTabs` | `ChatTab[]` | Брать активный (`t.isActive`) |
| `messages` | `ChatElement[]` | Основной контент — постить в Zulip |
| `pendingApprovals` | `Approval[]` | Отдельный пост с предупреждением |
| `agentStatus` | `AgentStatus` | Можно игнорировать или слать в activity |
| `agentActivityText` | `string\|null` | Текущая активность агента |
| `activeComposerId` | `string` | Для дедупликации (один агент в разных окнах) |
### Управление состоянием
Zulip не поддерживает редактирование сообщений так же гибко, как Telegram (может редактировать только контент существующего поста, не может менять топик/канал). Поэтому:
1. Новые `HumanMessage` → POST новый пост в топик
2. Новые `AssistantMessage` → POST новый пост
3. Изменяющиеся (streaming) → **edit_message** для последнего ai-сообщения
4. `ToolCallElement` (completed) → POST отдельным постом
5. `PlanBlock`, `RunCommand` → POST отдельным постом
6. `pendingApprovals` → POST с @**everyone** (если в конфиге)
Топик: `cursor/{windowTitle}/{tabTitle}`, где `windowTitle` — имя проекта.
### Потенциальные сложности
1. **Streaming** — Cursor агент пишет сообщения постепенно, DOM меняется каждые 300-500ms. Нужно редактировать последний пост пока сообщение не зафиксировалось. Иначе каждое слово — отдельный пост.
2. **Rate лимиты Zulip API** — много мелких апдейтов могут вызвать 429. Нужен debounce на редактирование.
3. **Дубликаты при multi-window** — когда два окна Cursor показывают одного агента (через global rail), нужна дедупликация по `activeComposerId`.
4. **Прерывание работы** — при перезапуске сервера не постить уже отправленные сообщения заново. Нужен message-tracker.
### Команды для разработки
```bash
cd ~/Developer/cursorremote
# Запуск сервера (без extension)
npm run dev # hot-reload, tsx watch
npm start # Production (после npm run build)
# Тесты (уже есть, свои добавить)
npm test
# Настройка через .env
cp .env.example .env
# Добавить: ZULIP_ENABLED=true, ZULIP_URL, ZULIP_BOT_EMAIL, ZULIP_BOT_API_KEY
# CDP включение Cursor
open -a Cursor --args --remote-debugging-port=9222
```
### Состояние проекта (26 июня 2026)
- ✅ Репозиторий склонирован: `~/Developer/cursorremote/`
- ✅ Код изучен, архитектура понятна
- ✅ analysis.md создан в Obsidian
- ❌ ZulipTransport не реализован
- ❌ config.ts не расширен
- ❌ .env не настроен
### Структура папок для Zulip транспорта (предлагаемая)
```
src/server/transports/zulip/
├── index.ts # ZulipTransport — implements Transport
├── zulip-api.ts # REST клиент для Zulip API (send, edit, fetch)
├── topic-manager.ts # windowTitle+tabTitle → топик/тема
└── message-tracker.ts # ChatElement.id → Zulip message_id
```