9.0 KiB
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 (наблюдение)
- CDPBridge (
cdp-bridge.ts) — подключается к Cursor через CDP WebSocket (--remote-debugging-port=9222), управляет жизненным циклом соединения - DOMExtractor (
dom-extractor.ts) — черезRuntime.evaluateдёргает DOM Cursor внутри его renderer'а, вытаскивает[data-flat-index]элементы, парсит их вChatElement[] - WindowMonitor — циклически опрашивает все окна Cursor, собирает
WindowSnapshot - StateManager (
state-manager.ts) — диффает новый state с предыдущим, эмититstate:patch - Транспорты подписываются на события 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 (может редактировать только контент существующего поста, не может менять топик/канал). Поэтому:
- Новые
HumanMessage→ POST новый пост в топик - Новые
AssistantMessage→ POST новый пост - Изменяющиеся (streaming) → edit_message для последнего ai-сообщения
ToolCallElement(completed) → POST отдельным постомPlanBlock,RunCommand→ POST отдельным постомpendingApprovals→ POST с @everyone (если в конфиге)
Топик: cursor/{windowTitle}/{tabTitle}, где windowTitle — имя проекта.
Потенциальные сложности
- Streaming — Cursor агент пишет сообщения постепенно, DOM меняется каждые 300-500ms. Нужно редактировать последний пост пока сообщение не зафиксировалось. Иначе каждое слово — отдельный пост.
- Rate лимиты Zulip API — много мелких апдейтов могут вызвать 429. Нужен debounce на редактирование.
- Дубликаты при multi-window — когда два окна Cursor показывают одного агента (через global rail), нужна дедупликация по
activeComposerId. - Прерывание работы — при перезапуске сервера не постить уже отправленные сообщения заново. Нужен 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