Files
obsidian-vault/personal/projects/cursor-to-zulip/analysis.md
T

185 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```