Иногда полезная инженерная работа начинается не с кода, а с разговора.

Например, вы обсуждаете архитектуру, ограничения, варианты реализации или странный баг в ChatGPT. Через какое-то время направление становится понятным, и дальше хочется продолжить уже локально: в репозитории, терминале, с Codex CLI.

Но контекст остался в ChatGPT.

Можно вручную скопировать summary, можно вставить отдельные фрагменты, можно начать новую сессию в Codex и заново объяснить задачу. Всё это работает, но теряет часть деталей: почему было принято решение, какие варианты отбросили, какие ограничения уже проговорили.

Мне захотелось сделать маленький мост:

ChatGPT share URL -> локальная сессия Codex CLI

Так появился chatgpt2codex.

npx chatgpt2codex https://chatgpt.com/share/<id>

Репозиторий: https://github.com/vv-bogdanov/chatgpt2codex
npm: https://www.npmjs.com/package/chatgpt2codex

Что делает утилита

chatgpt2codex принимает публичную ссылку на расшаренный ChatGPT-диалог и импортирует его как локальную сессию Codex CLI.

По умолчанию сессия привязывается к текущей директории:

npx chatgpt2codex https://chatgpt.com/share/<id>

Можно явно указать проект:

npx chatgpt2codex https://chatgpt.com/share/<id> -C /path/to/project

Можно сначала посмотреть, что будет импортировано, ничего не записывая:

npx chatgpt2codex https://chatgpt.com/share/<id> --dry-run

Если для директории уже есть сессия, утилита по умолчанию завершится с ошибкой. Для осознанной замены есть флаг:

npx chatgpt2codex https://chatgpt.com/share/<id> --force

Почему это не просто “записать файл”

Первая версия казалась очевидной: распарсить ChatGPT share page, нормализовать сообщения и записать JSONL-файл в директорию сессий Codex.

У Codex CLI локальные сессии лежат примерно здесь:

~/.codex/sessions/YYYY/MM/DD/

Файл называется в духе:

rollout-2026-06-30T11-50-01-<session-id>.jsonl

Внутри это JSONL: строка с метаданными сессии, дальше события диалога.

Но после первого импорта Codex не всегда подхватывал сессию в resume flow.

Оказалось, что в современных сборках одного JSONL недостаточно. Codex использует ещё и локальную SQLite-базу:

~/.codex/state_5.sqlite

Именно там хранится индекс тредов: cwd, title, preview, source, archived state, путь до rollout-файла и другие поля, которые нужны для отображения сессии в списке продолжения.

То есть рабочий импорт должен писать не только файл, но и согласованную запись в локальный state.

Формат на практике

Минимально полезный импорт делает несколько вещей.

Сначала создаёт rollout JSONL с метаданными сессии. Важно, чтобы сессия выглядела для Codex как нормальная CLI-сессия:

{
  "type": "session_meta",
  "payload": {
    "source": "cli",
    "thread_source": "user",
    "cwd": "/path/to/project"
  }
}

В ранней версии я использовал кастомный source, и это оказалось плохой идеей: Codex не воспринимал такую сессию как обычную CLI-сессию и мог фильтровать её из resume picker.

Затем утилита пишет строку в state_5.sqlite, если база существует. Для отображения важны, в частности:

  • id

  • rollout_path

  • cwd

  • source

  • title

  • preview

  • first_user_message

  • has_user_event

  • archived

После этого Codex уже видит импортированную сессию как локальный тред, который можно продолжить.

Обработка дублей

Я не хотел, чтобы импорт случайно перетирал существующий рабочий контекст.

Поэтому поведение по умолчанию такое:

если для cwd уже есть сессия -> выйти с ошибкой

Проверка смотрит сначала в SQLite-индекс Codex, потом fallback-ом по JSONL-файлам. Если пользователь действительно хочет заменить существующий импорт, он явно передаёт --force.

При --force утилита удаляет старую строку из SQLite, удаляет старый rollout-файл и создаёт новую сессию.

Парсинг ChatGPT share

Отдельная часть — достать саму переписку из share URL.

ChatGPT share page не является публичным API импорта. Поэтому парсер сделан прагматично: он извлекает данные из страницы, нормализует сообщения и приводит их к небольшой внутренней модели:

interface NormalizedMessage {
  role: "user" | "assistant";
  text: string;
  createdAt?: string;
}

Этого достаточно, чтобы перенести полезный контекст в Codex-сессию.

Тесты

Поскольку и ChatGPT share pages, и локальный формат Codex не являются стабильными публичными API, тесты здесь важнее, чем может показаться для маленькой CLI-утилиты.

Я добавил проверки на:

  • парсинг share-страницы;

  • запись rollout JSONL;

  • корректный source=cli;

  • запись в SQLite-индекс;

  • обнаружение существующей сессии для cwd;

  • поведение --force.

И отдельно проверил сценарий через локальный npm tarball, чтобы убедиться, что пакет работает не только из исходников.

Ограничения

Это не официальный импорт ChatGPT -> Codex.

Утилита опирается на текущие форматы ChatGPT share pages и локального состояния Codex CLI. Они могут измениться. Поэтому я держу реализацию небольшой, а поведение — максимально прямолинейным.

Ещё одно практическое ограничение: нужен Node.js 22.13.0 или новее, потому что для записи SQLite-индекса используется node:sqlite.

Зачем это всё

Главная идея простая: не терять контекст при переходе от обсуждения к реализации.

Если разговор в ChatGPT уже содержит ограничения, решения и аргументы, полезно уметь перенести его в локальный coding-agent workflow без ручного пересказа.

chatgpt2codex закрывает ровно этот небольшой зазор:

обсудили в ChatGPT -> расшарили URL -> продолжили в Codex CLI

Код открыт, пакет опубликован в npm: