diff --git a/personal/projects/cursor-to-zulip/analysis.md b/personal/projects/cursor-to-zulip/analysis.md new file mode 100644 index 00000000..37379700 --- /dev/null +++ b/personal/projects/cursor-to-zulip/analysis.md @@ -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; + stop(): Promise; +} +``` + +**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 { + // 1. Подписаться на windowMonitor.on('window:update') + // 2. Подписаться на stateManager.on('state:patch') + } + + async stop(): Promise { + // Отписаться + } +} +``` + +### Детали 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 +```