185 lines
9.0 KiB
Markdown
185 lines
9.0 KiB
Markdown
# 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
|
||
```
|