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

9.0 KiB
Raw Blame History

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):

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:

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',
},

Нужно добавить:

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 интерфейс напрямую:

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.

Команды для разработки

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