# session-import

[中文](README.md) · **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 via `ctx.sessions` and 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 `memory` seam).

**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)

```bash
# 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 activate
```

### Option B: local development / not yet published (link to the plugin dir)

```bash
# 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)

```bash
npm login                                   # any npm account (session-import is unscoped)
npm version patch|minor|major               # bump version
npm publish                                 # prepublishOnly runs build:all + test automatically
```

## Usage 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)

```bash
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 assertions
- `node 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 add` into a temp DSH_HOME → boot → import, self-contained)
- `node scripts/demo-codex.mjs <rollout.jsonl>` — import one real Codex session
- `node scripts/demo-scan.mjs <dir> [--import] [--source codex] [--redact] [--limit N]` — directory scan/batch import demo
- `node 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):

```ts
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:

```bash
node scripts/verify-dsh.mjs        # needs $DSH_HOME/profiles/ci-chat-headless (installed as above)
```

## Uninstall

```bash
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 store
```

## Adding a new source adapter

```ts
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 first
```

## Known boundaries

- Knowledge extraction defaults to the offline heuristic `HeuristicLlmJudge` (enabled with `defaultExtractKnowledge: 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.cwd` is written into the imported session's `SessionHeader.cwd` — the session lands in the matching workspace (sidebar group) and satisfies dsh's hard `cwd` requirement 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; `ICredentials` is wired to the real `ctx.credentials`.
