
DSH Chat Import
☆ 4从Claude Code、Codex、Reasonix等Agent工具导入历史消息,并在DSH中继续对话
Get this plugin
Review the source, then continue to the publisher.
dsh plugin add dsh-chat-import@latestAbout this plugin
Source snapshot 8/13/2026English | 简体中文
DSH Chat Import
Import Claude Code / Codex / ChatGPT / Cursor / Gemini / Reasonix / opencode conversation histories into DeepSeek Harness as resumable sessions.
Listed in: Awesome DeepSeek Harness · Awesome DSH Plugin · Awesome DSH Plugins · npm
Nwflower/dsh-chat-import adds external chat-history import to DeepSeek Harness: it brings Claude Code JSONL transcripts, Codex / ChatGPT CLI rollout JSONL, ChatGPT web-export conversations.json, Cursor agent transcripts, Gemini CLI session JSON, Reasonix session JSONL, and opencode SQLite history into DSH as full-fidelity, resumable sessions. The plugin never rewrites source files and never touches the DSH engine; every import appends a fresh, event-balanced session log through the public sessionPersistence service and attaches the session to the workspace of its cwd.
Features
- Import Claude Code transcripts: reads
~/.claude/projects/<slug>/<sessionId>.jsonl, parses user / assistant / tool / thinking messages. Only the main transcript (file name = recordedsessionId) becomes a session: auxiliary subagent / workflow fragment transcripts under<sessionId>/subagents/**(e.g.agent-*.jsonl) carry the parentsessionIdin their records and are skipped with a reason, so they can never shadow or collide with the main conversation. - Import Codex / ChatGPT CLI rollouts: reads
~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl(the OpenAI Codex CLI has been folded into ChatGPT; format unchanged), parsesresponse_itemmessages / function_call / custom_tool_call / reasoning. - Import ChatGPT web exports: reads
conversations.jsonfrom an exported ZIP (one file holds all conversations), rebuilds each conversation along the main mapping thread. - Import Cursor agent transcripts: reads
~/.cursor/projects/<slug>/agent-transcripts/<composer-id>/<composer-id>.jsonl, parses text / tool_use, filters[REDACTED]sentinels. - Import Gemini CLI sessions: reads
~/.gemini/history/<slot>/chats/session-*.json(one JSON object per file), parses user/gemini messages,thoughts→reasoning, and inlinetoolCalls(results live on the same object);infosystem notices are skipped. - Import Reasonix sessions: reads
~/.reasonix/sessions/desktop-*.jsonlandsubagent-sub-*.jsonl, parses user/assistant/tool messages (accepts both v1 nested and v2 flattool_calls),reasoning_content→reasoning, pairs tool results bytool_call_id;cwd/title come from the sibling<stem>.meta.json; V2 WAL sidecars (.events.jsonl/.conflicts.jsonl/.guardian.jsonl) are excluded from directory scans. - Import opencode sessions: reads the opencode SQLite database (
~/.local/share/opencode/opencode.db), rebuilds each session from thesession/message/parttables (theeventtable is a partial mirror andsession_message/session_inputare empty — ignored); text / reasoning / tool calls (with error flags) / image attachments / patches / subtasks are preserved, and model resolution follows the message-level → session-level fallback chain. Respects opencode's conversation compaction — compacted sessions import as their last summary (a leadingreasoningblock) plus the retained tail instead of the full history; opt back into the full history withfullHistory: true. - Full fidelity: tool history maps to
tool/call+tool/result(with error flags andsourceEventSeqslinkage), thinking blocks map toreasoning, multi-step assistant messages are preserved. - Resumable: synthesizes
turn/start,step/start,user/message,assistant/message,tool/call,tool/result,step/end,turn/endevents into a balanced, loadable session — open it and continue chatting. - Session metadata preserved: source
sessionId,cwd,ai-title(Claude; pinned assession/titleso auto-titles can't override), real model name (where the source records one), creation time. - Auto workspace attach: resolves/creates the workspace by
cwdandattachSessions, so imported sessions are grouped correctly (no more "ungrouped"); ChatGPT exports and Cursor transcripts carry nocwdand are left ungrouped. - Idempotent: skips when the target session already exists; malformed lines are counted and reported, never aborting the import.
- Batch import: pass a directory to
pathto recursively scan.jsonl(Claude / Codex / Cursor / Reasonix) or.json(ChatGPT / Gemini) files; each file becomes its own session (likewise each conversation inside a ChatGPT file), returning per-file / per-session summaries. Claude auxiliary transcripts (file name ≠ recordedsessionId) are skipped, never merged into the main session. For opencode, point at the data directory holdingopencode.db(or the.dbfile itself) to import every session in the database at once.
Design
Event-sourcing mapping
The plugin cuts the Claude Code transcript into turns on "direct human prompts": a record with type === 'user' and string content opens a new turn; every following assistant message (including tool_use / thinking blocks) is one step, and tool_result records attach to the most recent step. Each turn becomes one closed DSH round:
turn/start→step/start→user/message→assistant/message→(tool/call+tool/result)→step/end→turn/end.- Messages carry stable ids (
import:<sessionId>:u<turn>/:a<turn>:<step>/:t<turn>:<step>:<callId>) andsurfaceOp: 'append'. - Assistant
sourceis{ kind: 'model', provider: 'claude-code', model: <source model> };tool/resultsourceis{ kind: 'tool', callId }, linked to itstool/callviasourceEventSeqs.
Service dependencies
- The host only consumes public services:
sessionPersistence(create+append),fs(reading source files),tools(registering tools),workspaceRegistry(resolveByPath/create/attachSessiongrouping). - No services are published, so no isolate realm is needed.
- Host-only plugin: no Browser side.
Data model
Claude Code JSONL
| Claude Code JSONL | DSH SessionEvent |
|---|---|
{ type: "user", message.content: string } (direct prompt) | turn/start + step/start + user/message |
{ type: "assistant", content: [{ type: "text", text }] } | assistant/message |
{ type: "assistant", content: [{ type: "thinking", … }] } | reasoning content block |
{ type: "assistant", content: [{ type: "tool_use", … }] } | tool/call + tool-call content block |
{ type: "user", content: [{ type: "tool_result", … }] } | tool/result (sourceEventSeqs links its tool/call) |
| turn ends | step/end + turn/end |
Storage: ~/.claude/projects/<slug>/<sessionId>.jsonl for the main transcript; the sibling <sessionId>/subagents/** directory holds auxiliary fragment transcripts (agent-*.jsonl, workflow journals) whose records reuse the parent sessionId. Imports only treat the main transcript (file name = recorded sessionId) as a session — auxiliary files are skipped with a reason instead of being imported as (or merged into) the main session, which would otherwise let whichever file sorts first shadow the full conversation.
Codex / ChatGPT CLI rollout
Line envelope: { timestamp, type, payload }. The event_msg user_message / agent_message are duplicates of response_item records and are ignored to avoid double counting; user message blocks starting with < (<environment_context>, <user_instructions>, <system-reminder>, …) are harness injections and never enter the prompt.
| Codex rollout | DSH SessionEvent |
|---|---|
session_meta / turn_context | SessionHeader (id / cwd / createdAt / model) |
response_item message role=user (input_text) | turn/start + step/start + user/message |
response_item message role=assistant (output_text) | assistant/message |
response_item function_call / custom_tool_call | tool/call (attached to the latest assistant step) |
response_item function_call_output / custom_tool_call_output | tool/result (paired by call_id across lines, sourceEventSeqs linkage) |
response_item reasoning | skipped (content is encrypted, unreadable) |
| turn ends | step/end + turn/end |
ChatGPT web export conversations.json
The top level is a JSON array (one file holds all conversations); each conversation object has a mapping (a DAG: nodeId → { id, message, parent, children }). Traverse from the root along the active branch (last children entry) to rebuild the main thread; placeholder nodes with message: null and author.role === 'system' are skipped; timestamps are Unix seconds (×1000 → ms). ChatGPT is a chat, the export has no cwd, so sessions are not grouped into workspaces.
| conversations.json | DSH SessionEvent |
|---|---|
conversation object (id / title / create_time) | SessionHeader (id / createdAt) + session/title |
mapping node with author.role: "user" | turn/start + step/start + user/message |
node with author.role: "assistant" | assistant/message |
node with author.role: "tool" | tool/result (attached to the latest step) |
author.role: "system" / message: null | skipped |
| turn ends | step/end + turn/end |
Cursor agent transcript
Line structure: { role: "user" | "assistant", message: { content: [...] } }, no envelope. Content has only text / tool_use blocks (input is already an object). The first user message is wrapped in <user_query> tags (stripped); assistant text frequently contains "[REDACTED]" sentinels (client-side privacy stripping, filtered); the transcript contains no tool_result (tool results live only in the UI bubble store) → only call history is imported; there are no timestamps / model, the session id comes from the file name (composer uuid), and there is no cwd.
| Cursor transcript | DSH SessionEvent |
|---|---|
role: "user" (text wrapped in <user_query>) | turn/start + step/start + user/message |
role: "assistant" text blocks | assistant/message |
role: "assistant" tool_use blocks | tool/call (no tool/result — the transcript has no results) |
[REDACTED] sentinels | filtered |
| turn ends | step/end + turn/end |
Gemini CLI session JSON
Storage: ~/.gemini/history/<slot>/chats/session-*.json — one JSON object per file (not JSONL). Top level: { sessionId, projectHash, startTime, directories, kind, messages: [...] }. Message types: user (content is a parts array) starts a turn; gemini (string content, optional thoughts and toolCalls) is one assistant step; info (CLI system notices such as error banners / cancellations) is skipped. Tool results are inline on the same object as the call (unlike Claude's split messages).
| Gemini session JSON | DSH SessionEvent |
|---|---|
top-level (sessionId / startTime / directories[0]) | SessionHeader (id / createdAt / cwd) |
type: "user" (parts array) | turn/start + step/start + user/message |
type: "gemini" string content | assistant/message |
thoughts entries | reasoning content blocks |
toolCalls[].args + inline result | tool/call + tool/result (same step, sourceEventSeqs linkage; status: "error" → isError) |
type: "info" | skipped |
| turn ends | step/end + turn/end |
Reasonix session JSONL
Storage: ~/.reasonix/sessions/<stem>.jsonl (desktop-* desktop sessions / subagent-sub-* sub-agent sessions), one session per file. Lines are OpenAI-style messages without an envelope, and both format generations are accepted: v1 tool_calls use the nested { id, type: "function", function: { name, arguments } } shape, v2 uses the flat { id, name, arguments } shape and may carry a createdAt (unix ms). Tool results (role: "tool" with tool_call_id) pair with the assistant tool_calls[].id. A sibling <stem>.meta.json provides workspace → cwd and summary → pinned title. When neither the transcript nor the meta carries a timestamp, the session creation time falls back to the one embedded in the file name (desktop-YYYYMMDDHHMM-* / subagent-sub-*-YYYYMMDDHHMM). V2 WAL sidecars (<stem>.events.jsonl, .conflicts.jsonl, .guardian.jsonl) are append-only logs, not transcripts, and are excluded from directory scans.
| Reasonix JSONL | DSH SessionEvent |
|---|---|
role: "user" (string content) | turn/start + step/start + user/message |
role: "assistant" string content | assistant/message |
reasoning_content | reasoning content block |
tool_calls[].function (v1 nested / v2 flat) | tool/call |
role: "tool" with tool_call_id | tool/result (paired by tool_call_id, sourceEventSeqs linkage) |
<stem>.meta.json (workspace / summary) | cwd / session/title |
| turn ends | step/end + turn/end |
opencode session database (SQLite)
Storage: ~/.local/share/opencode/opencode.db (SQLite, WAL). The importer reads the session / message / part tables — each message/part row's data is a JSON document; messages sort by time_created, id, parts likewise. The event table is only a partial mirror of sessions and session_message / session_input are empty, so all three are ignored. Tool results live inline in the tool part's state (unlike Claude's split messages), so tool/call + tool/result are emitted together from one part; even a tool part without output still emits an empty result so calls and results stay paired. opencode compacts long conversations: a compaction part carries a tail_start_id, and the mode: "compaction" summary message that follows holds the summary text. By default the importer keeps only the last compaction's summary (a leading reasoning block) plus the messages from tail_start_id onward, matching opencode's actual compacted context; fullHistory: true imports every message instead.
| opencode DB | DSH SessionEvent |
|---|---|
session row (id / title / directory / time_created / model) | SessionHeader (id / createdAt / cwd) + session/title |
message with role: "user" (text parts) | turn/start + step/start + user/message |
message with role: "assistant" | assistant/message (one per message) |
part type: "text" | text content block |
part type: "reasoning" | reasoning content block |
part type: "tool" | tool/call + tool/result (same step, sourceEventSeqs linkage; state.status === "error" → isError) |
part type: "file" | text block [image: <filename>] |
part type: "patch" | text block [patch: <N> files] |
part type: "subtask" | text block [subtask: <command> — <description>] |
part type: "step-start" / "step-finish" | skipped (structural) |
part type: "compaction" (tail_start_id) | drop the pre-tail_start_id history; its summary message becomes a leading reasoning block |
| turn ends | step/end + turn/end |
SessionHeader: version: 0, id: import-<source sessionId>, createdAt (source timestamp; for Reasonix the file-name timestamp, import time when the source has neither, e.g. Cursor), cwd (source working directory; absent for ChatGPT exports and Cursor transcripts).
Build
Pure ESM, no build step: index.mjs is the Host plugin entry (the conversion core lives in dependency-free convert.mjs, unit-testable on its own); no tsc / bundling.
Install
dsh plugin --profile web add -w link:/path/to/dsh-chat-import
Or, once published to npm:
dsh plugin --profile web add dsh-chat-import
dsh plugin is a pnpm forwarder: after add it reads the dsh.bundle declaration, folds the cordis.patch.yml insert lines into the profile's bundles, and the plugin is active after restarting dsh. For local development a link: (symlink) is recommended.
Compatibility
- Dependency surface: consumes only public host plugin APIs (
sessionPersistence/fs/tools/workspaceRegistry) and@deepseek-ai/dsh-tools(declared aspeerDependencies, tested against0.1.0-rc.6).
| Source format | Location | Import tool | Verified |
|---|---|---|---|
| Claude Code | ~/.claude/projects/<slug>/<sessionId>.jsonl | import_claude | ✅ 44 tool/call + 44 tool/result persisted, load OK |
| Codex / ChatGPT CLI | ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl | import_codex | ✅ unit + mock integration (npm test) |
| ChatGPT web export | ZIP → conversations.json | import_chatgpt | ✅ unit + mock integration (npm test) |
| Cursor | ~/.cursor/projects/<slug>/agent-transcripts/<id>/<id>.jsonl | import_cursor | ✅ unit + mock integration (npm test) |
| Gemini CLI | ~/.gemini/history/<slot>/chats/session-*.json | import_gemini | ✅ unit + mock integration (npm test) |
| Reasonix | ~/.reasonix/sessions/desktop-*.jsonl (and subagent-sub-*.jsonl) | import_reasonix | ✅ unit + mock integration (npm test); dry-run on 55 real sessions |
| opencode | ~/.local/share/opencode/opencode.db (SQLite) | import_opencode | ✅ unit + mock integration (npm test) |
- Verified: 2026-08 on
dsh 0.1.0-rc.6web profile — full "import → resume → workspace attach" run;npm test(68 cases) covers the pure conversion logic and mock integration paths for all seven source formats.
Uninstall
Remove import-claude from the profile's bundles (the insert line in cordis.patch.yml) and restart dsh; the plugin stops loading. Already-imported sessions stay in the DSH data directory and are unaffected.
Usage
Note: imports persist to disk immediately, but the DSH session list does not auto-refresh — refresh the page (or the session list) after importing to see the new sessions.
In a session with this plugin mounted, call the tools:
import_claude({ path: "C:\\Users\\<you>\\.claude\\projects\\<slug>\\<sessionId>.jsonl" })
import_codex({ path: "C:\\Users\\<you>\\.codex\\sessions\\2026\\05\\18\\rollout-2026-05-18T21-14-16-xxxx.jsonl" })
import_chatgpt({ path: "C:\\Users\\<you>\\Downloads\\chatgpt-export\\conversations.json" })
import_cursor({ path: "C:\\Users\\<you>\\.cursor\\projects\\<slug>\\agent-transcripts\\<composer-id>\\<composer-id>.jsonl" })
import_gemini({ path: "C:\\Users\\<you>\\.gemini\\history\\<slot>\\chats\\session-2026-04-17T18-09-b26d7f99.json" })
import_reasonix({ path: "C:\\Users\\<you>\\.reasonix\\sessions\\desktop-202606020721-1.jsonl" })
import_opencode({ path: "C:\\Users\\<you>\\.local\\share\\opencode\\opencode.db" })
import_claude / import_codex / import_cursor / import_gemini / import_reasonix behave alike: path can be a single file or a directory; optional sessionId overrides the target DSH session id (default import-<source sessionId>; Cursor uses the file-name composer id, Reasonix uses the file-name stem). They return { mode: 'single', sessionId, turns, messages, toolCalls, skipped, alreadyImported }; after importing, refresh the session list to see the new session, already attached to its working directory.
import_chatgpt differs: conversations.json holds all conversations in one file, so even a single file returns the batch shape { mode: 'batch', total, imported, alreadyImported, skipped, failed, results: [...] } (total is the conversation count, each results entry is one conversation); ChatGPT exports have no cwd, so imported sessions are not grouped into workspaces.
import_opencode also always returns the batch shape: one opencode.db holds all sessions, so total is the session count and each results entry is one session. path may be the .db file or the data directory containing it; optional sessionIds (array of source session ids) restricts the import to the listed sessions; optional fullHistory: true imports the full message history, ignoring opencode's compaction (default false — compacted sessions import as their last summary plus the retained tail). Imported sessions keep their directory as cwd and are grouped into workspaces.
Batch import (directory)
import_claude({ path: "C:\\Users\\<you>\\.claude\\projects" })
import_codex({ path: "C:\\Users\\<you>\\.codex\\sessions" })
import_chatgpt({ path: "C:\\Users\\<you>\\Downloads\\chatgpt-export" })
import_cursor({ path: "C:\\Users\\<you>\\.cursor\\projects" })
import_gemini({ path: "C:\\Users\\<you>\\.gemini\\history" })
import_reasonix({ path: "C:\\Users\\<you>\\.reasonix\\sessions" })
import_opencode({ path: "C:\\Users\\<you>\\.local\\share\\opencode" })
Directory mode recursively scans (recursive: false for top level only) all .jsonl (Claude / Codex / Cursor / Reasonix) or .json (ChatGPT / Gemini) files; each file imports as one session (likewise each conversation inside a ChatGPT file); non-transcript / empty files are skipped, Claude auxiliary transcripts (file name ≠ recorded sessionId) are skipped with a reason, and Reasonix V2 WAL sidecars (.events.jsonl / .conflicts.jsonl / .guardian.jsonl) are excluded. import_opencode directory mode simply locates opencode.db in the given directory (no recursion) and imports every session it holds. Returns { mode: 'batch', total, imported, alreadyImported, skipped, failed, results: [...] }, where each results entry carries path, status (imported / already-imported / skipped / failed) and session stats.
Scope & boundaries
- Source transcripts are read-only, never rewritten in place; DSH history events are likewise append-only (deep-frozen) — new events are added, existing ones are never modified.
- Does not modify the DSH engine, apiproxy, or official UI packages; publishes no services, so no isolate realm is needed.
- Reading transcripts outside the workspace requires the session sandbox to allow access to that path.
- Known boundaries: auxiliary records like
permission/summaryare not imported;tool_resultwithis_errorkeeps the error flag but drops fields beyondmessage.content; Claude subagent / workflow fragment transcripts (file name ≠ recordedsessionId) are skipped — only the main<sessionId>.jsonlbecomes a session; Codexreasoningcontent is encrypted and unreadable, so it is skipped (planned for v1.2); ChatGPT exports rebuild only the main thread (branch = last child), tool messages attach to the nearest step as text without restoring the tool-argument structure; Cursor transcripts contain notool_result(results live only in the UI bubble store) — onlytool/callhistory is imported, and[REDACTED]text is filtered; Gemini imports follow observed format as of 2026-04 (Gemini publishes no stable schema) —thoughtsmap toreasoning, inline tool results are honored when present; Reasonix imports read the.jsonltranscript checkpoint (the V2.events.jsonlWAL is excluded — event-log-only sessions newer than the checkpoint are not covered); opencode imports read themessage+parttables (theeventtable is only a partial mirror andsession_message/session_inputare empty, so they are ignored),patchparts carry no diff content so only a placeholder[patch: <N> files]block is emitted, and tool output may contain ANSI escapes that are kept verbatim; opencode's conversation compaction is respected by default — the pre-tail_start_idhistory is folded into the last summary (a leadingreasoningblock), andfullHistory: trueimports the full history instead; seven source formats are supported today: Claude Code JSONL, Codex / ChatGPT CLI rollout, ChatGPT web export, Cursor agent transcripts, Gemini CLI sessions, Reasonix sessions, and opencode.
Tests
npm test
test/convert.test.mjs covers the pure conversion logic for all seven source formats (turn balance, tool linkage, titles, malformed lines, injection filtering, duplicate-message dedup, mapping branches / placeholder nodes, REDACTED filtering, inline tool results, v1/v2 tool-call shapes, opencode part mapping and model fallback); test/index.test.mjs runs the full apply → execute path with mock fs / sessionPersistence / tools / workspaceRegistry (and a real SQLite temp DB for import_opencode) and validates the return value against the output schema.