ai-data-extractor — выгрузка истории чатов ИИ-ассистентов в JSONL
📂 Исходный код на GitHubБесплатный инструмент с открытым исходным кодом для выгрузки истории чатов ИИ-ассистентов в один нормализованный формат JSONL. Поддерживает Claude Code, Codex CLI, Cursor, Windsurf, Trae, Continue, Gemini CLI, OpenCode, Cline / Roo Code и Aider. Только стандартная библиотека Python, лицензия MIT.
Что это за проект
ai-data-extractor — это набор скриптов на Python, который вытаскивает историю ваших разговоров с ИИ-ассистентами в один общий формат JSONL. Он читает данные прямо с вашего компьютера: из папок, которые оставляют Claude Code, Cursor, Windsurf, Aider и другие инструменты.
Зачем это нужно. Во-первых, резервная копия: у многих приложений история чатов лежит в локальной базе, которую приложение может стереть при переустановке. Во-вторых, свои разборы: можно посчитать, сколько токенов ушло на задачу и какие файлы вы правили. В-третьих, подготовка данных для обучения моделей — авторы проекта прямо называют fine-tuning одной из причин.
Скрипты берут только ваши данные, которые лежат на вашей же машине, под вашей учётной записью.
Что нужно для запуска
Зависимостей нет. Нужен Python версии 3.9 или новее, авторы рекомендуют 3.10. Лицензия — MIT.
Проверить, что всё на месте, и сразу запустить извлечение:
python3 --version # need 3.9+, 3.10+ recommended
python3 extract.py --list
python3 extract.py --all
--list — безопасный предпросмотр: скрипт покажет, какие источники найдены, но ничего не запишет. --all вытаскивает всё, что поддерживается, без вопросов. Есть и короткий вариант:
./extract_all.sh
Какие инструменты поддерживаются
Десять источников. У каждого своё место хранения и свой формат:
| # | Инструмент | Формат хранения | Где искать |
|---|---|---|---|
| 1 | Claude Code | JSONL, файл на сессию | ~/.claude/projects/**/*.jsonl |
| 2 | Codex CLI | JSONL-файлы rollout | ~/.codex/sessions/**/rollout-*.jsonl |
| 3 | Cursor | SQLite (state.vscdb) |
~/…/Cursor/User/{global,workspace}Storage |
| 4 | Windsurf | SQLite, схема не документирована | ~/…/Windsurf/User/{global,workspace}Storage |
| 5 | Trae | SQLite + JSONL, схема не документирована | ~/…/Trae |
| 6 | Continue | JSON, файл на сессию | ~/.continue/sessions/*.json |
| 7 | Gemini CLI | JSON, файл на чат | ~/.gemini/tmp/<hash>/chats/*.json |
| 8 | OpenCode | JSON (деревья session/message/part) | ~/.local/share/opencode/storage/ |
| 9 | Cline / Roo Code | JSON, папка на задачу | <editor>/User/globalStorage/<ext-id>/tasks/ |
| 10 | Aider | Markdown-транскрипт на проект | <project>/.aider.chat.history.md |
Скрипт сам определяет операционную систему и сам собирает список подходящих каталогов: ~/Library/Application Support, ~/.config, ~/.local/share, %APPDATA%, %LOCALAPPDATA%. Объяснять операционную систему в командной строке не нужно.
Как это работает
Пять шагов:
- Определить ОС и собрать список возможных корней данных.
- В каждом корне найти папку с известным именем нужного инструмента.
- Прочитать хранилище. Формат зависит от инструмента: JSONL построчно, JSON-дерево или SQLite.
- Привести всё к общей схеме
messages[]. - Записать результат в JSONL, по одной беседе на строку, в папку
extracted_data/.
Два момента, которые авторы выносят отдельно. Первый: SQLite открывается только на чтение. Из-за этого запущенный редактор не блокирует извлечение. Второй: каждый чтец обёрнут в защиту, поэтому один битый или занятый файл не роняет весь запуск — скрипт отдаст частичный результат и пойдёт дальше.
Что получается на выходе
Каждый запуск создаёт файлы с меткой времени в папке extracted_data/:
extracted_data/
├── claude_code_conversations_20260816_143022.jsonl
├── cursor_conversations_20260816_143022.jsonl
├── aider_conversations_20260816_143022.jsonl
├── cline_conversations_20260816_143022.jsonl
└── all_conversations.jsonl
Одна строка — одна беседа:
{
"messages": [
{
"role": "user",
"content": "How do I fix this TypeScript error?",
"code_context": [
{"file": "/Users/you/project/src/index.ts", "code": "const x: string = 123;"}
],
"timestamp": "2026-01-16T14:30:22Z"
},
{
"role": "assistant",
"content": "The error occurs because you're assigning a number to a string type...",
"tool_use": [{"name": "edit_file", "input": {"path": "src/index.ts"}}],
"timestamp": "2026-01-16T14:30:25Z"
}
],
"source": "cursor-composer",
"session_id": "c1a2b3...",
"project_path": "/Users/you/project",
"name": "TypeScript Type Error Fix",
"created_at": 1705414222000
}
Состав полей зависит от инструмента. Не каждый хранит code_context, расход токенов или путь к проекту. Гарантированы только messages, source и session_id — на них и стоит опираться в своём коде.
Ключи командной строки
python3 extract.py [--all] [--sources ids] [--list] [--output-dir DIR]
[--search-path PATH ...] [--merge]
--all— извлечь все источники без запросов.--sources ids— список источников через запятую. Меню не показывается.--list— показать, что найдено, ничего не извлекая.--output-dir DIR— куда писать JSONL. По умолчанию./extracted_data.--search-path PATH— дополнительный каталог для поиска, можно указать несколько раз.--merge— после извлечения ещё и склеить всё вall_conversations.jsonl.
Отдельный экстрактор тоже можно запустить сам по себе: python3 -m extractors.cursor из корня проекта или python3 extractors/cursor.py. Это удобно, когда разбираешься с одним источником.
Проблемы, на которые можно наступить
«No installation found». Инструмент либо не установлен, либо истории чатов ещё нет, либо она лежит в нестандартном месте. Укажите каталог через --search-path или поправьте константы SEARCH_DIRS и APP_DIR_NAMES в файле нужного экстрактора.
База Cursor или Windsurf занята. Чтение идёт в режиме mode=ro, но если ошибка всё же появилась — закройте приложение и повторите запуск.
Windsurf или Trae найдены, но бесед ноль. Эвристика не узнала текущие ключи хранилища. Сначала подтвердите, что папка приложения найдена, потом посмотрите ключи напрямую:
sqlite3 state.vscdb "SELECT key FROM ItemTable"
Дальше добавьте нужные значения в KEY_HINTS.
Про Cursor, Windsurf и Trae
Эти три инструмента не публикуют схему своего хранилища, и она менялась уже несколько раз. Только у Cursor схема менялась трижды: сначала чат в ItemTable на уровне рабочей области, потом встроенный композер, потом композер с разделённым bubbleId. Экстрактор для Cursor учитывает все три известных варианта явно.
Для Windsurf и Trae сделан общий подход на эвристике — extractors/common.py, функция heuristic_extract_chat_from_kv. Она перебирает ключи, связанные с чатами, и обходит распарсенный JSON в поисках объектов вида «роль плюс текст». Это честная лучшая попытка, а не документированный формат. Если будущая версия приложения всё поменяет и перестанет совпадать — это ожидаемо. Тогда правится KEY_HINTS в нужном файле.
Про Aider
У Aider нет общей базы сессий. В каждом каталоге проекта лежит свой файл .aider.chat.history.md. По умолчанию набор сканирует домашний каталог и несколько типовых имён корня проекта — projects, code, dev, repos, workspace, src, Documents — на глубине до пяти каталогов. Папки node_modules, .git и подобные пропускаются. Если проекты лежат в другом месте, укажите их прямо:
python3 extract.py --sources aider --search-path ~/client-work --search-path /mnt/data/repos
Как добавить свой источник
Каждый экстрактор — маленький модуль с двумя функциями. Скопируйте самый простой (continue_ext.py — хороший образец) и заполните:
DISPLAY_NAME = "My Tool"
SOURCE_ID = "my_tool"
def find_installations(extra_paths: list[Path] | None = None) -> list[Path]:
"""Return the directories/files worth scanning."""
def extract(installations: list[Path]) -> list[dict]:
"""Return a list of conversation dicts matching the schema above."""
Потом зарегистрируйте модуль в списке REGISTRY в extract.py. В extractors/common.py лежат готовые читатели для SQLite, JSON и JSONL, а также обе эвристики, которые понадобятся почти всегда.
Данные для обучения моделей
Готовый пример на Hugging Face:
from datasets import load_dataset
dataset = load_dataset("json", data_files="extracted_data/*.jsonl", split="train")
dataset = dataset.filter(lambda x: any(m["role"] == "assistant" for m in x["messages"]))
def format_chat(example):
return {"text": tokenizer.apply_chat_template(example["messages"], tokenize=False)}
dataset = dataset.map(format_chat)
Приватность
Перед тем как что-то отдавать или на чём-то обучать, авторы советуют проверить результат на секреты:
pip install detect-secrets --break-system-packages
detect-secrets scan extracted_data/*.jsonl
Дальше посмотрите глазами, не попали ли в выгрузку ключи, внутренний код клиента или личные пути. Больше всего подозрений вызывают поля code_context и tool_use. Папку extracted_data/ не коммитят в публичный репозиторий — она уже в .gitignore. Если там клиентская работа, держите её на зашифрованном диске.
Отдельно в README идёт оговорка: вы отвечаете за то, что имеете права на извлечённые данные, за аккуратную работу с чувствительной информацией и за соблюдение условий использования каждого инструмента.