Экспорт истории чатов Cursor (composer-чаты, режимы Agent/Ask/Edit) из локальной
базы state.vscdb в Markdown-файлы, сгруппированные по workspace'ам.
Написан взамен заброшенного somogyijanos/cursor-chat-export:
тот читает старый ключ workbench.panel.aichat.view.aichat.chatdata из
workspace-баз, которого в современных версиях Cursor больше нет — вся история
переехала в глобальную базу.
- uv — зависимости объявлены inline (PEP 723),
uv сам создаёт изолированное окружение; никаких
pip installи ручных venv. Python 3.10+ uv тоже поставит сам, если его нет. - Linux с Cursor'ом в стандартном месте:
~/.config/Cursor/User/(для macOS/Windows путь передаётся через--db, см. ниже)
install.sh сам проверит/установит uv, скачает скрипт, запустит его через
uv run в изолированном окружении и удалит временные файлы:
# Список чатов
curl -fsSL https://raw.githubusercontent.com/MushroomSquad/cursor-export/main/install.sh \
| bash -s -- --list
# Экспортировать всё в ./export/
curl -fsSL https://raw.githubusercontent.com/MushroomSquad/cursor-export/main/install.sh \
| bash -s --
# С фильтром и thinking-блоками
curl -fsSL https://raw.githubusercontent.com/MushroomSquad/cursor-export/main/install.sh \
| bash -s -- --query airflow --thinkingДля приватного форка задай токен с repo-скоупом и добавь заголовок к curl:
export GITHUB_TOKEN=$(gh auth token) curl -fsSL -H "Authorization: token $GITHUB_TOKEN" https://raw.githubusercontent.com/<owner>/cursor-export/main/install.sh | bash -s -- --list
install.shиспользуетGITHUB_TOKENиз окружения и при скачивании самого скрипта.
URL скрипта можно переопределить:
CURSOR_EXPORT_URL="https://.../export_cursor_chats.py".
# Через uv (создаст окружение сам)
uv run --script export_cursor_chats.py --list
# Или напрямую — shebang сам вызывает uv
./export_cursor_chats.py --list
# Экспортировать всё в ./export/<workspace>/<дата>_<название>_<id>.md
./export_cursor_chats.py
# Только чаты, где в названии или тексте встречается подстрока (без учёта регистра)
./export_cursor_chats.py --query airflow
# --list и --query вместе: найти, не экспортируя
./export_cursor_chats.py --list --query nebula
# Включить thinking-блоки ассистента (сворачиваемые <details> в Markdown)
./export_cursor_chats.py --thinking
# Нестандартный путь к базе и папке вывода
./export_cursor_chats.py --db /path/to/state.vscdb --out /path/to/outputТе же флаги работают и в curl-варианте — всё после bash -s -- передаётся
скрипту как есть.
Скрипт можно запускать при работающем Cursor: перед чтением каждая база
копируется во временную папку (вместе с -wal/-shm), поэтому блокировок
и рваных чтений не будет.
| ОС | Путь |
|---|---|
| Linux | ~/.config/Cursor/User/globalStorage/state.vscdb |
| macOS | ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb |
| Windows | %APPDATA%\Cursor\User\globalStorage\state.vscdb |
На macOS/Windows глобальную базу можно передать через --db, но привязка к
workspace'ам ищется по захардкоженному линуксовому пути ~/.config/Cursor/User/
(константа CURSOR_USER_DIR в начале скрипта) — при переносе поправь её.
Один Markdown-файл на чат:
# Название чата
- **Workspace:** infra
- **Created:** 2026-07-05 21:25
- **Composer ID:** `1dc02c3f-...`
- **Messages:** 5
## User
текст запроса...
## Assistant
> 🔧 `read_file_v2` {"path": "..."}
текст ответа...Вызовы инструментов ассистента рендерятся однострочниками > 🔧 ...
(аргументы обрезаются до 200 символов), результаты инструментов не
экспортируются — иначе файлы раздуваются на порядок.
Вся история — в глобальной базе
~/.config/Cursor/User/globalStorage/state.vscdb, таблица cursorDiskKV
(обычная SQLite key-value: колонки key, value с JSON):
| Ключ | Содержимое |
|---|---|
composerData:<composerId> |
Метаданные чата: name, createdAt (unix ms), fullConversationHeadersOnly — упорядоченный список {bubbleId, type} |
bubbleId:<composerId>:<bubbleId> |
Одно сообщение: type (1 = юзер, 2 = ассистент), text, richText, toolFormerData (вызов инструмента: name, rawArgs), allThinkingBlocks, createdAt |
Часть значений в таблице может быть NULL — скрипт такие записи пропускает.
Привязка чата к проекту берётся из workspace-баз
~/.config/Cursor/User/workspaceStorage/<hash>/:
state.vscdb, таблицаItemTable, ключcomposer.composerData→ JSON со спискомallComposers(composerId, название, даты) этого workspace;workspace.jsonрядом →folderс URI папки проекта.
Чаты, которых нет ни в одном workspace (удалённые workspace'ы, фоновые
агенты), попадают в папку export/unknown-workspace/ — содержимое при этом
экспортируется полностью.
Формат хранения Cursor меняет без предупреждения. Первым делом посмотри, какие ключи реально лежат в базах:
# Префиксы ключей в глобальной базе (чаты должны быть в composerData/bubbleId)
sqlite3 ~/.config/Cursor/User/globalStorage/state.vscdb \
"SELECT substr(key,1,instr(key||':',':')) p, count(*) FROM cursorDiskKV GROUP BY p ORDER BY 2 DESC LIMIT 15;"
# Ключи чатов в workspace-базе
sqlite3 ~/.config/Cursor/User/workspaceStorage/<hash>/state.vscdb \
"SELECT key FROM ItemTable WHERE key LIKE '%chat%' OR key LIKE '%composer%' OR key LIKE '%aiService%';"
# Посмотреть структуру конкретной записи
sqlite3 ~/.config/Cursor/User/globalStorage/state.vscdb \
"SELECT value FROM cursorDiskKV WHERE key LIKE 'composerData:%' LIMIT 1;" | python3 -m json.tool | head -50Дальше правишь под новые ключи три места в скрипте: SQL-запрос по
composerData:%, сборку ключа bubbleId:... и load_workspace_map().
export/ добавлен в .gitignore не случайно: экспортированные файлы — это
полная переписка с ассистентом, включая куски кода, пути, логи и всё, что
попадало в контекст. Не коммить их и не выкладывай наружу.