session-import
中文 · English
Cross-tool session migration plugin for the DeepSeek Harness (dsh) — a real dsh bundle plugin:
- 15+ source session import (ChatGPT / Claude Code / Codex / Kimi Code / ZCode / WorkBuddy / OpenClaw / Gemini / Cursor / Aider / Continue / Cline / OpenCode / Reasonix / GitHub Copilot Chat / generic JSON);
- Imports produce native dsh session-log events (
turn/start·user/message·assistant/message(with thinking reasoning blocks and tool-call blocks) ·tool/call·tool/result·turn/end), written viactx.sessionsand persisted to the official JSONL backend — imported sessions render natively in the dsh Web UI and can be continued; - Bidirectional export (dsh session → generic JSON / Markdown / transcript);
- Privacy redaction detection (API Key / phone / email / ID card / IP / bank card);
- Knowledge extraction (decisions / code / conventions → deduplicated local memory, via the
memoryseam).
Design docs: see [docs/](docs/) — [PRD](docs/PRD-session-import.md) · [ARCH](docs/ARCH-session-import.md) · [TECH](docs/TECH-session-import.md).
Implemented (mapped to FR/NFR/AC)
| Module | File | Requirements |
|---|---|---|
| Pluggable parser adapters (14+ sources) | src/parsers/*.ts | FR-01/02/03/16 |
| Adapter registry + detection | src/registry.ts | FR-02 — detect() sniffing + generic fallback |
| Real dsh session-event mapping | src/mapping.ts | FR-04/05/06 — native Session.append vocabulary (turn/user/assistant/tool), thinking→reasoning blocks, unknown tools→tool/result.meta.unknownTool |
| Privacy redaction | src/privacy.ts | FR-15/AC-3 |
| Bidirectional export (event inversion) | src/exporter.ts | FR-13 |
| Knowledge extraction | src/knowledge.ts, src/heuristicJudge.ts | FR-11/12 |
| Import orchestration | src/importService.ts | FR-07/08/09/14/15/16 — conflict rename/merge/skip, archive, robust SkipError |
| Streaming chunked import | src/streamImport.ts | FR-10/NFR-02 |
| Real dsh seam adapters | src/dsh/dshRuntime.ts | ctx.sessions / ctx.sessionTitle / ctx.credentials; archives/history/memory under $DSH_HOME/session-import |
| Cordis plugin entry | src/plugin.ts | apply(ctx, config): sessionImport service, /session-import command, /api/session-import/* REST routes |
| Bundle manifest | cordis.patch.yml, package.json#dsh.bundle | Standard dsh profile bundle protocol |
| Standalone dev wizard (optional) | src/server.ts, public/* | Local wizard without the harness (npm run wizard) |
Install into dsh
Option A: from the npm registry (simplest)
# pnpm 9.x needs the -w flag for workspace-root installs (dsh plugin forwards pnpm args verbatim)
dsh plugin --profile web add -w session-import
# dsh auto-adds the package to the profile's dsh.profile.bundles (it detects the
# dsh.bundle declaration); restart dsh to activateOption B: local development / not yet published (link to the plugin dir)
# Build once in the plugin directory (produces dist/ and lib/)
npm install && npm run build && npm run bundle:client
# Install into your profile (web example)
dsh plugin --profile web add -w link:D:/work/DeepSeekHarness/Plugins/session-import
# or manually: add "session-import": "link:<absolute-path>" to profile/package.json
# dependencies and append "session-import" to dsh.profile.bundles, then pnpm install there
# Restart dsh, then:
# - Web UI: /session-import <export-file-path> [--redact] [--extract-knowledge]
# - or REST: POST /api/session-import/import> Windows note: pnpm 9.x has a known issue resolving file:D:/... absolute paths (it > mangles the drive letter into the profile dir). Use the link:<absolute-path> > specifier for local development (pnpm creates a junction). Registry installs are unaffected.
Publishing to npm (maintainers)
npm login # any npm account (session-import is unscoped)
npm version patch|minor|major # bump version
npm publish # prepublishOnly runs build:all + test automaticallyUsage surfaces inside dsh
0. Web import wizard (multi-select checkboxes, browser UI plugin) — open any session → top Tab "Import": - Full candidate list: auto-scans all registered sources (Codex / Claude Code / …), showing title/messages/size/age per row; - Multi-select: checkboxes + select-all/clear, then import the confirmed files as individual dsh sessions; - Title option: tick "derive titles from first message" so unnamed Codex sessions get a title from the first user message (approximates Codex auto-naming / desktop preview); unticked uses the timestamp title; - Import results shown inline (session ids / skip reasons), list auto-refreshes (sidebar too). - Data path: the browser talks to /api/session-import/scan-dir and /api/session-import/import-paths on the host webserver. 1. Conversational discovery/import (model tools) — the agent drives "scan → confirm → import": - chat_import_discover: probes the well-known local session dirs (see the table below), returns a numbered candidate list (title/path/messages/size/age), read-only; supports titleFromFirstMessage. - chat_import_import: imports the user-confirmed paths as new dsh sessions (supports redact / extractKnowledge / titleFromFirstMessage); failed files are skipped with reasons; titles come from the source (Codex thread names / Claude Code ai-title), unnamed ones fall back to a readable timestamp or the first user message when titleFromFirstMessage is set. - Example: > User: import my recent Codex sessions > Agent: chat_import_discover → list candidates → User: import 1、3、5 → Agent: chat_import_import → summary - On by default; disable with bundle config enableModelTools: false. 2. /session-import slash command (Web UI / any commands adapter): `` /session-import C:\path\to\chatgpt\conversations.json # single file /session-import C:\Users\PC\.codex\sessions # directory (recursive scan) /session-import C:\Users\PC\.codex\sessions --source codex # codex only /session-import <path> --redact --extract-knowledge # redact + knowledge extraction ` Directory imports return a summary (imported 21/21 session(s) (codex×21)); bad files are skipped without aborting (FR-16). 3. sessionImport service (other plugins / scripts): `ts const svc = ctx.get('sessionImport') // { ingest, sources, detectSource, parseCount, scanDirectory, ingestDirectory } const preview = await svc.scanDirectory('C:/Users/PC/.codex/sessions', { sourceFilter: ['codex'] }) const batch = await svc.ingestDirectory('C:/Users/PC/.codex/sessions', { sourceFilter: ['codex'], redact: true }) // batch: { dir, total, imported: ImportResult[], skipped: {path,reason}[], ignored: number } ` 4. REST API (web profile, registered on the host webserver): - GET /api/session-import/adapters — registered source ids - POST /api/session-import/import — { fileName, text, options } single-file import - POST /api/session-import/scan-dir — { sourceFilter?, maxFiles?, titleFromFirstMessage? } discovery scan (returns numbered candidate groups with titles, no writes) - POST /api/session-import/import-dir — { dir, options, sourceFilter?, maxFiles? } batch directory import - POST /api/session-import/import-paths — { paths, options } import confirmed paths (wizard/conversation shared) - GET /api/session-import/history — import history 5. Data landing: sessions → official dsh session-log ($DSH_HOME/sessions, JSONL, grouped by workspace bound to the source cwd); original archives / import history / local memory → $DSH_HOME/session-import/ (override with bundle config dataDir`).
Source adapter coverage (mainstream agent frameworks)
| Framework | source id | Auto-discovery dir (env override) | Parser | Status |
|---|---|---|---|---|
| Codex CLI | codex | ~/.codex/sessions (CODEX_HOME) | Dedicated (rollout + legacy; title chain: session_index.jsonl thread name → optional first-user-message → timestamp fallback; session_meta.cwd binding) | ✅ verified against real sessions |
| Claude Code | claude_code | ~/.claude/projects (CLAUDE_CONFIG_DIR) | Dedicated (session-based + legacy; ai-title title, cwd binding) | ✅ verified against real sessions |
| Kimi Code | kimi_code | ~/.kimi-code/sessions (KIMI_CODE_HOME) | Dedicated v2 (real wire.jsonl event stream: turn.prompt/context.append_loop_event (content.part think/text, tool.call/tool.result); state.json title + session_index.jsonl workdir; sub-agents agents/agent-N too) | ✅ verified against real sessions |
| ZCode (Zhipu GLM) | zcode | ~/.zcode/cli (ZCODE_HOME) | Dedicated v2 (both real formats: sub-agent transcript.jsonl (turn_started/model_streaming/tool_call_scheduled/turn_complete) + main-session rollout/model-io-sess_*.jsonl (model_io snapshots with reasoning/toolCalls, system-reminder/task-notification noise filtered); metadata.json → title description + cwd binding) | ✅ verified against real sessions (main 32 msgs + sub-agent) |
| WorkBuddy / CodeBuddy (Tencent) | workbuddy | ~/.codebuddy (CODEBUDDY_HOME) | OpenAI message format / generic JSON (exported sessions) | ✅ synthetic import verified (no local sessions) |
| OpenClaw (claw.so) | openclaw | ~/.openclaw/agents (OPENCLAW_HOME) | Dedicated (real session log: session (cwd binding) / message (text / thinking / toolCall / toolResult blocks)) | ✅ verified against real sessions |
| Gemini CLI | gemini | ~/.gemini (GEMINI_HOME) | Dedicated (chats.history) | ✅ synthetic import verified (not installed) |
| Cursor | cursor | ~/.cursor (CURSOR_HOME) | Dedicated (source:cursor/toolCalls) | ✅ synthetic import verified (not installed) |
| Aider | aider | ~/.aider (AIDER_HOME) | Dedicated (markdown history) | ✅ synthetic import verified (not installed) |
| Continue | continue | ~/.continue/history (CONTINUE_HOME) | OpenAI message format (files detect as openai_family) | ✅ synthetic import verified (not installed) |
| OpenCode | opencode | ~/.opencode/sessions (OPENCODE_HOME) | OpenAI message format (files detect as openai_family) | ✅ synthetic import verified (not installed) |
| Cline / Copilot Chat exports | openai_family | custom CHAT_IMPORT_OPENAI_DIR | OpenAI message format | ✅ synthetic import verified |
| ChatGPT | chatgpt | manual export file | Dedicated (mapping graph) | ✅ unit-test covered |
| Any JSON | generic_json | manual file | generic fallback | ✅ |
> Each framework joins auto-discovery with one row in AGENT_SOURCES (src/discovery.ts); > SQLite-backed frameworks (new Cline/Continue, Trae, 通义灵码, DevChat, …) integrate via exported JSON; local sqlite parsing is future work. > chat_import_discover / the wizard only list directories that actually exist on the machine. > Verification: node scripts/verify-all-sources.mjs — detect → parse → import for every registered framework (real sessions when present: codex / claude_code / kimi_code / zcode / openclaw; synthetic fixtures otherwise: gemini / cursor / aider / continue / opencode / openai_family / workbuddy).
Development without the harness (headless / standalone wizard)
npm test # 89 cases (node --import tsx --test)
npm run build # tsc strict → dist/
npm run wizard # standalone import wizard http://localhost:4173 (pure local-file seams)E2E scripts (real dsh profile):
node scripts/verify-dsh.mjs— service/command/import/persistence assertionsnode scripts/verify-all-sources.mjs— detect → parse → import for all registered frameworks (real/synthetic)node scripts/verify-zcode.mjs— ZCode main rollout + sub-agent real import (title/cwd persistence)node scripts/verify-install.mjs— tarball-install usability check (npm pack→dsh plugin addinto a temp DSH_HOME → boot → import, self-contained)node scripts/demo-codex.mjs <rollout.jsonl>— import one real Codex sessionnode scripts/demo-scan.mjs <dir> [--import] [--source codex] [--redact] [--limit N]— directory scan/batch import demonode scripts/demo-discover.mjs [--import-limit N]— model-tool loop: discover real candidates → import → persistence check
> These scripts boot a real dsh installation (dsh-app-boot). The dsh checkout path is > resolved by scripts/lib/dsh-env.mjs: $DSH_CHECKOUT env > dsh CLI on PATH > npm global root. > No script edits needed — install dsh on PATH or set DSH_CHECKOUT to run on another machine.
CI (GitHub Actions, .github/workflows/ci.yml): on push/PR runs Node 18/20/22 matrix of typecheck → build → client bundle → unit tests → npm pack content check, plus a dsh-integration job (installs the dsh CLI, runs verify-dsh + verify-all-sources synthetic fixtures).
Minimal usage (no harness):
import { buildHeadlessContainer } from './dist/plugin.js';
import { HeuristicLlmJudge } from './dist/heuristicJudge.js';
const { service } = buildHeadlessContainer(new HeuristicLlmJudge());
const result = await service.ingest(
{ path: 'codex-session.jsonl', text: '<Codex transcript JSONL>' },
{ redact: true, extractKnowledge: true },
);
console.log(result.sessionId, result.messageCount);End-to-end verification (real dsh)
scripts/verify-dsh.mjs boots a profile containing this plugin exactly like the dsh CLI does, and asserts: sessionImport service available, /session-import command registered, imports produce real session events, and JSONL persistence succeeds:
node scripts/verify-dsh.mjs # needs $DSH_HOME/profiles/ci-chat-headless (installed as above)Uninstall
dsh plugin --profile web remove session-import
# or remove the dependency and bundles entry from the profile package.json, then restart the harness
# Historical data: deleting $DSH_HOME/session-import clears archives/history/memory; sessions remain in the dsh session storeAdding a new source adapter
import { ParserAdapter, RawInput, msg } from './src/parsers/base.js';
export class MyToolAdapter implements ParserAdapter {
readonly source = 'mytool';
readonly schemaVersion = '1.0';
detect(raw: RawInput) { /* sniff distinctive features, avoid false positives */ return false; }
parse(raw: RawInput) { return { source: this.source, messages: [] }; }
}
// register(new MyToolAdapter()) in createDefaultRegistry() (src/registry.ts), specific sources firstKnown boundaries
- Knowledge extraction defaults to the offline heuristic
HeuristicLlmJudge(enabled withdefaultExtractKnowledge: true); wiring a real LLM channel (ctx.llm) is future work. - Imported sessions are named
import-<uuid>to avoid collisions with the store's per-process counter ids and existing on-disk logs. - Sessions bind to the source working directory: Codex rollout
session_meta.cwdis written into the imported session'sSessionHeader.cwd— the session lands in the matching workspace (sidebar group) and satisfies dsh's hardcwdrequirement for cold resume / model selection (sessions without cwd cannot resume). - Session titles: Codex thread names come from
~/.codex/session_index.jsonl(thread_name) and are applied automatically on import (session/title, user source pinned, never overwritten by fallbacks); unnamed sessions get dsh's first-message fallback. - dsh currently has no memory-service seam, so knowledge sinks to plugin-local files;
ICredentialsis wired to the realctx.credentials.