dsh-chat-import
Health Warn
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 6 GitHub stars
Code Pass
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
从Claude Code、Codex、Reasonix等Agent工具导入历史消息,并在DSH中继续对话
DSH Chat Import
Bring your Claude Code, Codex, ChatGPT, Cursor, Gemini, Reasonix and opencode conversations into DeepSeek Harness — and keep talking exactly where you left off.
Listed in: Awesome DeepSeek Harness · Awesome DSH Plugin · Awesome DSH Plugins · npm
dsh-chat-import turns your external agent chat history into full-fidelity, resumable DeepSeek Harness sessions — tool calls, reasoning and all. It reads transcripts read-only (never rewrites your source files), never touches the DSH engine, and appends every import as a fresh, event-balanced session log through the public sessionPersistence service, grouped into the workspace of its cwd.
7 sources · Copy-only · Seamlessly resumable · Auto workspace grouping
✨ Features
- 📥 Import from 7 sources — Claude Code JSONL, Codex / ChatGPT CLI rollouts, ChatGPT web exports, Cursor agent transcripts, Gemini CLI sessions, Reasonix sessions, and opencode SQLite history. One plugin, one call per source.
- 🔍 Full fidelity — tool history becomes real
tool/call+tool/resultpairs (error flags andsourceEventSeqslinkage included), thinking blocks becomereasoning, multi-step assistant messages are preserved. - ▶️ Seamlessly resumable — every import synthesizes a balanced, loadable session (
turn/start→step/start→user/message→assistant/message→tool/call/tool/result→step/end→turn/end): open it and keep chatting. - 🗂 Auto workspace grouping — sessions land in the workspace of their source
cwd(no more "ungrouped"); session id, title, model and creation time are preserved where the source records them. - 🔁 Idempotent — re-importing skips sessions that already exist; malformed lines are counted and reported, never aborting the import.
- 📦 Batch import — point at a directory (or the whole opencode DB) and every file / conversation becomes its own session, with a per-file summary.
🚀 Quick start
# 1. Install (npm package)
dsh plugin --profile web add dsh-chat-import
# or from a local checkout
dsh plugin --profile web add -w link:/path/to/dsh-chat-import
- In any DSH session, import a single file or a whole directory:
import_claude({ path: "~/.claude/projects" })
- Refresh the session list once, open the imported session, and continue chatting — it resumes exactly where the source left off.
🗂 What can I import?
| Source | Storage location | Import tool |
|---|---|---|
| Claude Code | ~/.claude/projects/<slug>/<sessionId>.jsonl |
import_claude |
| Codex / ChatGPT CLI | ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl |
import_codex |
| ChatGPT (web export) | exported ZIP → conversations.json |
import_chatgpt |
| Cursor | ~/.cursor/projects/<slug>/agent-transcripts/<id>/<id>.jsonl |
import_cursor |
| Gemini CLI | ~/.gemini/history/<slot>/chats/session-*.json |
import_gemini |
| Reasonix | ~/.reasonix/sessions/desktop-*.jsonl |
import_reasonix |
| opencode | ~/.local/share/opencode/opencode.db (SQLite) |
import_opencode |
Each import preserves what the source actually records — session id, cwd, title, model, creation time, tool calls & results, reasoning — and formats with less data (Cursor transcripts, ChatGPT exports) import what exists and clearly report what they don’t.
🛠 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.
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:
pathcan be a single file or a directory (directories are scanned recursively; each file becomes its own session).- Optional
sessionIdoverrides the target DSH session id (defaultimport-<source sessionId>; Cursor uses the file-name composer id, Reasonix the file-name stem). - Returns
{ mode: 'single', sessionId, turns, messages, toolCalls, skipped, alreadyImported }.
import_chatgpt differs: one conversations.json holds all conversations, so even a single file returns the batch shape { mode: 'batch', total, imported, alreadyImported, skipped, failed, results: [...] } (each results entry is one conversation). ChatGPT exports carry no cwd, so imported sessions are not grouped into workspaces.
import_opencode also always returns the batch shape — one opencode.db holds all sessions. path may be the .db file or its data directory; optional sessionIds restricts the import to the listed sessions; optional fullHistory: true imports the full message history instead of respecting opencode’s conversation 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.
🧩 Data model
The importer cuts each transcript into turns on "direct human prompts" (a user record with string content), and synthesizes one closed DSH round per turn:
turn/start → step/start → user/message → assistant/message → (tool/call + tool/result) → step/end → turn/end
Messages carry stable ids and surfaceOp: 'append'; tool/result events link back to their tool/call via sourceEventSeqs. Assistant source is { kind: 'model', provider: 'claude-code', model: <source model> }; tool/result source is { kind: 'tool', callId }. The SessionHeader keeps version: 0, id: import-<source sessionId>, source createdAt and cwd.
Claude Code JSONL
Main transcript at ~/.claude/projects/<slug>/<sessionId>.jsonl; auxiliary subagent / workflow fragments under <sessionId>/subagents/** reuse the parent sessionId and are skipped (they can never shadow or merge into the main conversation).
| 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 |
Codex / ChatGPT CLI rollout
Line envelope: { timestamp, type, payload }. event_msg user/agent messages duplicate response_item records and are ignored; user blocks starting with < (<environment_context>, <user_instructions>, …) are harness injections and never enter the prompt. Codex reasoning content is encrypted and skipped.
| 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 |
response_item function_call_output / custom_tool_call_output |
tool/result (paired by call_id, sourceEventSeqs linkage) |
response_item reasoning |
skipped (encrypted, unreadable) |
| turn ends | step/end + turn/end |
ChatGPT web export (conversations.json)
Top level is a JSON array (one file, all conversations); each conversation has a mapping DAG. The active branch (last children entry) is rebuilt as the main thread; placeholder nodes with message: null and author.role === 'system' are skipped; timestamps are Unix seconds (×1000 → ms). No cwd exists, so sessions are not grouped.
| conversations.json | DSH SessionEvent |
|---|---|
conversation object (id / title / create_time) |
SessionHeader + 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: [...] } }. First user message is wrapped in <user_query> (stripped); [REDACTED] sentinels are filtered. Transcripts contain no tool_result (results live only in the UI bubble store) and no timestamps / model — the session id comes from the file name, and there is no cwd.
| Cursor transcript | DSH SessionEvent |
|---|---|
role: "user" (text in <user_query>) |
turn/start + step/start + user/message |
role: "assistant" text blocks |
assistant/message |
role: "assistant" tool_use blocks |
tool/call (no result in transcript) |
[REDACTED] sentinels |
filtered |
| turn ends | step/end + turn/end |
Gemini CLI session JSON
One JSON object per file at ~/.gemini/history/<slot>/chats/session-*.json. Message types: user (parts array) opens a turn; gemini (string content, optional thoughts and toolCalls) is one assistant step; info (CLI notices) is skipped. Tool results are inline on the same object as the call.
| 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 (status: "error" → isError) |
type: "info" |
skipped |
| turn ends | step/end + turn/end |
Reasonix session JSONL
OpenAI-style messages without envelope at ~/.reasonix/sessions/<stem>.jsonl; both v1 (nested { id, type: "function", function: { name, arguments } }) and v2 (flat { id, name, arguments }) tool_calls are accepted. Tool results (role: "tool" with tool_call_id) pair by 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 creation time falls back to the one embedded in the file name. V2 WAL sidecars (.events.jsonl / .conflicts.jsonl / .guardian.jsonl) 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) |
<stem>.meta.json (workspace / summary) |
cwd / session/title |
| turn ends | step/end + turn/end |
opencode session database (SQLite)
Reads the session / message / part tables of ~/.local/share/opencode/opencode.db (the event table is only a partial mirror and session_message / session_input are empty — ignored). Tool results are inline in the tool part’s state, so tool/call + tool/result are emitted together; a tool part without output still emits an empty result so calls and results stay paired. opencode compaction is respected by default: only the last compaction summary (a leading reasoning block) plus the messages from tail_start_id onward are imported; fullHistory: true imports everything.
| opencode DB | DSH SessionEvent |
|---|---|
session row (id / title / directory / time_created / model) |
SessionHeader + session/title |
message with role: "user" (text parts) |
turn/start + step/start + user/message |
message with role: "assistant" |
assistant/message |
part type: "text" |
text content block |
part type: "reasoning" |
reasoning content block |
part type: "tool" |
tool/call + tool/result (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: "compaction" (tail_start_id) |
drop pre-tail_start_id history; summary becomes leading reasoning |
| turn ends | step/end + turn/end |
⚙️ Compatibility
- Consumes only public host plugin APIs (
sessionPersistence/fs/tools/workspaceRegistry) and@deepseek-ai/dsh-tools(declaredpeerDependencies, tested against0.1.0-rc.6).
| Source format | Import tool | Verified |
|---|---|---|
| Claude Code | import_claude |
✅ 44 tool/call + 44 tool/result persisted, load OK |
| Codex / ChatGPT CLI | import_codex |
✅ unit + mock integration (npm test) |
| ChatGPT web export | import_chatgpt |
✅ unit + mock integration (npm test) |
| Cursor | import_cursor |
✅ unit + mock integration (npm test) |
| Gemini CLI | import_gemini |
✅ unit + mock integration (npm test) |
| Reasonix | import_reasonix |
✅ unit + mock integration (npm test); dry-run on 55 real sessions |
| opencode | import_opencode |
✅ unit + mock integration (npm test) |
- Verified: 2026-08 on
dsh 0.1.0-rc.6(web profile) — full "import → resume → workspace attach" run;npm test(79 cases) covers the pure conversion logic and mock integration paths for all seven source formats.
🔒 Safety & boundaries
- Source transcripts are read-only, never rewritten; DSH history events are append-only (deep-frozen) — new events are added, existing ones are never modified.
- The plugin never modifies the DSH engine, apiproxy, or official UI packages; it 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:
permission/summaryauxiliary records are not imported;tool_resultwithis_errorkeeps the error flag but drops fields beyondmessage.content; Claude subagent / workflow fragment transcripts are skipped (only the main<sessionId>.jsonlbecomes a session); Codexreasoningis encrypted and skipped; ChatGPT exports rebuild only the main thread (branch = last child) and tool messages attach to the nearest step as text; Cursor transcripts have notool_resultand[REDACTED]text is filtered; Gemini follows the format observed 2026-04 (no stable official schema); Reasonix reads the JSONL checkpoint (the V2 WAL is excluded); opencodepatchparts carry no diff (placeholder[patch: <N> files]only) and tool output may keep ANSI escapes verbatim.
🧪 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, dedup, mapping branches, 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.
📦 Install & uninstall
dsh plugin --profile web add dsh-chat-import # npm package
dsh plugin --profile web add -w link:/path/to/dsh-chat-import # local checkout (symlink, recommended for development)
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.
To uninstall, remove the import-claude insert line from the profile’s bundles and restart dsh. Already-imported sessions stay in the DSH data directory and are unaffected.
📄 License
MIT — see LICENSE.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found