[2026-06-26] eagle: personal/projects/cursor-to-zulip/analysis.md
This commit is contained in:
@@ -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
|
||||
```
|
||||
Reference in New Issue
Block a user