# dsh-packer > **Agent Configuration Packer**: pack your local DSH assets into standard zip archives — migrate, share, restore, with a privacy scan guarding every pack. > > [简体中文](README.md) · [English](README.en.md) > **v0.7.2** · MIT License · DSH 0.2.x (`>=0.2.0-rc.2 <0.3.0-0`, tested on the 0.2.1-rc.3 desktop app) · Node ≥ 22.19.0 **dsh-packer** is the Agent Configuration Packer plugin for **DeepSeek Harness (DSH)**: it packs your local Agent assets, module by module, into standard zip archives, for two purposes: - **Migration** — move your entire setup to a new machine, or recover after a reinstall. - **Sharing** — hand Skills and other assets to other people, with sensitive content filtered automatically. Every module is optional (Skills / Sessions / Attachments / Profiles / Settings / Presets / Memory); restore ships with diff reports and conflict strategies, and a privacy scan guards every pack — all capabilities are opt-in. ## Features | Feature | Description | | --- | --- | | Modular packing | Nine modules, any combination: `skills` / `sessions` / `attachments` (**the bytes of images/files inside sessions**, on by default for migration) / `profiles` / `plugins` (**installed plugin bodies**, on by default for migration) / `settings` / `presets` / `memory` (Markdown mirror) / `memoryDb` (**consistent SQLite snapshot of the memory truth source**, on by default) | | Two built-in presets | **Migrate** (everything, including installed plugin bodies and session attachments) / **Share** (Skills only; sessions, attachments, memory data, plugins and personal skill subdirectories are automatically excluded) | | Privacy & security scan | Detects drive-letter / Unix / UNC paths, user-directory paths, credential assignments and bare key shapes (`sk-`, `ghp_`, `AKIA`, JWT, …), plus personal nicknames, counting **every occurrence**; **share mode force-sanitizes every hit (it never blocks on a hit), migrate mode reports only** | | File-level operation preview | Full file list preview before packing; diff report before restore (added / changed / same / skipped) | | Three conflict strategies | Overwrite / skip / merge (merge appends, never overwrites existing content) | | Safe restore writes | Targets are backed up to `.restore-backups//` inside the packs directory first, written to a temp file and swapped in with an atomic `rename`; any failure **aborts and rolls back** the files already replaced/added | | Manifest integrity check | `manifest.json` records schemaVersion plus a SHA-256 fingerprint per file; source files are verified against it before restore, and a **missing or malformed fingerprint is rejected outright** (fail-closed) | | Pack management & notes | Pack list (time / size / modules / note), delete, rename, and a note written at pack time | | Share packs ship a README | A generated `README.md` describing the pack contents is attached automatically | | Dark-mode ready | The settings page follows the DSH theme (`--dsw-alias-*` tokens plus a brand-amber dual palette, dual-channel detection) | | Zero native dependencies | Standard zips built by a pure-JS ZIP writer (zip.mjs + node:zlib); openable by any unzip tool | | Selective restore | Pick the **modules** to restore (Skills only, without touching sessions) and toggle `includeDb` separately to restore the memory truth-source database | | Per-file diff | Importing a pack immediately computes added / changed / same / skipped, file by file, and the result card reports the backup directory | | One-click rollback | `/restore/backups` lists every pre-restore backup; `/restore/rollback` (the UI's "Roll back" button) restores the files that run overwrote | | Pack management | Browse a pack's file list with **inline SHA-256 verification**, rename, delete (deleted packs go to the **platform's own trash**: Recycle Bin on Windows, Trash on macOS, freedesktop Trash on Linux — all recoverable; HarmonyOS has none, so the UI warns that deletion is permanent) | | Encrypted packs | `AES-256-GCM` with a `scrypt`-derived key (`PKENC1` container); manifests and restores require the passphrase, and a wrong passphrase fails closed | | Progress & cancel | Packing and restoring run as background jobs (`/jobs/:id`) with a `done/total` percentage in the UI and a cancel button (honoured at file boundaries, no half-written zip) | ## Installation ### Desktop app: install by package name (recommended) 1. Open the DSH desktop app → **Plugins** in the sidebar → **Add plugin**; 2. Type the package name `dsh-packer` and click **Install** (it is enabled automatically). CLI equivalent: ```bash # replace desktop with your profile name dsh plugin --profile desktop add dsh-packer ``` ### From GitHub (alternative) ```bash # Requires git dsh plugin --profile desktop add github:KLRSL/dsh-packer ``` ### As a local bundle (development / link) ```bash # Run from the project directory dsh plugin --profile desktop add link:./dsh-packer ``` `dsh plugin` registers the package into `dsh.profile.bundles` and mounts the patch automatically; restart DSH afterwards. **Verify the install**: 1. Open **Settings → the "Config Packer" tab** — you should see the module checkboxes and the pack list. 2. Or run `/pack list` in the terminal — an empty list (or "no packs yet") means the command registered fine. 3. Run `/pack create --dry-run` once — it previews the file list and scan results without producing a zip, confirming the module paths are readable. ## Quick start ### Packing (three steps) ```bash # 1. Create a migrate pack (everything selected by default) /pack create --note "moving to a new machine" # 2. Or create a share pack (Skills only, sensitive content excluded) /pack create --share --note "for a friend" # 3. Preview first without generating a zip /pack create --mode migrate --dry-run ``` When it finishes, the zip is written to `~/.dsh/packs/` (override with `DSH_PACKS_DIR`); share packs automatically include a `README.md`. ### Restoring (three steps) ```bash # 1. Import the pack /pack restore ~/.dsh/packs/dsh-packer-2026-09-05-223045-migrate.zip # 2. Review the diff report (added / changed / same / skipped) # 3. Pick a conflict strategy: overwrite | skip | merge /pack restore --strategy merge ``` `manifest.json` is validated automatically (schemaVersion + fingerprint validity + every source file's SHA-256) before anything is applied; each target is backed up first and swapped in atomically, and a failure aborts the batch and rolls it back. Restart DSH if needed afterwards. ## Packable modules | Module | Contents | Migrate preset | Share preset | | --- | --- | --- | --- | | `skills` | Skills (including the memory-mechanism skill), under `~/.dsh/skills` | ✅ | ✅ | | `sessions` | Session records (`.zstd` format), under `~/.dsh/sessions` | ✅ | ❌ | | `attachments` | **Session attachment object store**: the bytes of images/files inside sessions (content-addressed, under `~/.dsh/attachments/v1/objects`; excludes the rebuildable request-image cache and the `tmp` staging area) | ✅ | ❌ | | `profiles` | Profile configs (excluding `node_modules`), under `~/.dsh/profiles` | ✅ | ❌ | | `plugins` | **Installed plugin bodies** (top-level package directories in `profiles//node_modules`); large | ✅ | ❌ | | `settings` | Global settings (`settings.yaml`) | ✅ | ❌ | | `presets` | Agent presets (`.agent-presets`) | ✅ | ❌ | | `memory` | **Memory mirror** (human-readable Markdown under `DSH_MEMORY_ROOT` or `~/.dsh/memory`, excluding `backups/`) | ✅ | ❌ | | `memoryDb` | **Memory truth source** (`/biomemory.db`): packed as a **consistent `VACUUM INTO` snapshot** (read-only connection, never touches a live WAL database); on failure the file is left out and reported under `unreadable` | ✅ | ❌ | **Session images live in `attachments`**: session logs only store an `attachmentId` reference (`sha256:<64hex>`); the bytes live under `~/.dsh/attachments/v1/objects`. Packing `sessions` alone would leave images broken after a restore (the history opens, but the image bytes are missing), so the **"Session records" group** in the settings page packs `sessions` + `attachments` together. Objects are content-addressed, so the target machine needs no reference rewriting. **The two built-in presets**: - **Migrate** — every module checked by default: ideal for relocating a whole environment. - **Share** — only `skills` is checked; sessions, attachments and memory data are excluded automatically, and personal skill subdirectories (`_shared`) are also excluded, keeping sensitive content out of the pack as much as possible. ## Privacy & security **Scan rules** (scope: files with known text extensions plus extensionless files that are clearly text, e.g. `.env`; binaries containing NUL bytes are skipped): | Rule | Description | | --- | --- | | Local absolute paths (drive letter) | Drive-letter style paths such as `D:\...`, `C:/...` | | Unix home paths | `/home//...`, `/Users//...`, `/root/...` — **home directories only** (the path says whose machine it is). `/usr`, `/etc`, `/var`, `/opt`, `/srv`, `/mnt`, `/media` and `/tmp` are shared system paths with no personal information: since v0.7.0 they are **neither reported nor sanitized** | | UNC / network paths | `\\server\share\...` | | User-directory paths | Paths under the OS user profile directory (`C:\Users\`, `/home/`, `/Users/`); mount prefixes in front of the home directory are consumed too (`/System/Volumes/Data/Users//…`, `/Volumes//Users//…`, `/mnt|media//Users//…`), so no half-sanitized prefix is left behind | | Mount / derived paths | `/Volumes/`, `/System/Volumes/Data`, `/private`, `/nix/store` — these are **machine-specific** (volume names are user-chosen, `/private` carries a per-machine hash, `/nix/store` exposes the local package layout), unlike `/usr` or `/etc`, which are identical on every machine. Replaced with `<本机路径>`, sub-path kept | | Suspected credentials / tokens | Assignments such as `api_key`, `access_key`, `secret`, `password`, `token`, `bearer`, `authorization`, `credential` — **quoted or unquoted** | | Bare key shapes | The keys themselves: `sk-...`, `ghp_...`, `github_pat_...`, `glpat-...`, `AKIA...`, `xox?-...`, JWTs (`eyJ.....*.*`) | | Personal nicknames | User nickname text (injected by the deployer via `config.personalPatterns`) | Counts are tallied per file + rule + line, so multiple hits on one line are all counted (no more "one hit per rule per file"). Scan cache: "Preview" and "Pack" both hit the same `/create` endpoint; the backend caches scan results by **file name + content SHA-256**, so the same content is never read and matched twice. **Change the content and the fingerprint changes, forcing a re-scan** — this is a performance optimization, not a correctness trade-off (calls without a SHA-256 are never cached, and editing the deployer's personal patterns invalidates the whole cache). **Blocking policy**: - **Share mode** — a hit is **never blocked, but sanitizing is forced**: the `sanitize` flag sent by the UI is ignored and every pack is sanitized (paths become placeholders, credentials become `<已移除的密钥>`; only the copy inside the pack changes, source files are untouched). Share packs are meant for others, so content is rewritten rather than passed through. Two things *do* fail a share pack: selecting a module the share preset forbids (**only `skills` is allowed**; anything else → `share_scope`), and files whose content cannot be read and therefore never went through the scan (fail-closed, the file list is reported). - **Migrate mode** — hits are reported only; inspect them in advance with `/pack scan` or `--dry-run` and decide yourself, or opt into "sanitize before packing". **Never packed**: `.credentials.yaml` and `.anonymous-user-id` are skipped in every module's file walk, regardless of mode — local identity/credential files never enter a pack. Other security measures: - Every file's **SHA-256** fingerprint is recorded in `manifest.json` for fail-closed integrity checks on restore; entries **without a well-formed fingerprint are rejected**, and files whose hash cannot be computed are skipped at pack time and reported (a blank fingerprint is never written). - Restore containment is **two-sided**: the source must stay inside the extraction directory and the target must stay inside its module root (zip-slip / tampered manifests with `../` escapes are rejected). Absolute paths, drive-letter paths, UNC paths and `..` segments are rejected by a whitelist before restore even starts. - Before unpacking, archive members are enumerated from the ZIP central directory by the plugin's own reader (no external commands) and whitelisted (absolute / drive-letter / UNC / `..` names rejected), and symlink / hardlink / device members are refused; the extracted tree is scanned again for symlinks. Temporary extraction directories are always cleaned up in `try/finally` — on success and on failure. - Targets are backed up first (`/.restore-backups//`), written to a temp file and swapped in with an atomic `rename`; any failure aborts and rolls back what this run already replaced or added. ### How memory migrates (read this) Since **v0.2.6 the memory truth source is in the pack**: the `memoryDb` module stores a consistent `VACUUM INTO` snapshot of `/biomemory.db` (entries plus audit records) instead of copying a live, DSH-held WAL database — a plain copy of a live WAL database would not be safe, hence the snapshot. If the snapshot cannot be taken the file is skipped and listed under `unreadable` rather than silently dropped. | Module | Contents | Enough for migration? | | --- | --- | --- | | `memoryDb` | **`VACUUM INTO` consistent snapshot** of `biomemory.db` (entries + audit) | ✅ the memory itself | | `memory` | Read-only Markdown mirror under `DSH_MEMORY_ROOT`, meant for humans | ⚠️ readable copy only | Restore still skips `*.db*` by default (`includeDb: true` writes them) so a memory database in use on the target machine is never overwritten — **stop DSH before restoring** if you want `memoryDb` written back. - SQLite is handled on both sides: **packing** takes a consistent `VACUUM INTO` snapshot (`memoryDb`, since v0.2.6) instead of copying a live, DSH-held WAL database (failure means the file is skipped and listed under `unreadable`); **restore** still skips `*.db*` by default (`includeDb: true` writes them) so a memory database in use on the target machine is never overwritten. - Pack file names carry a unique suffix (`dsh-packer---.zip`) so packs created within the same second never overwrite each other. - Packs are built by the plugin own **pure-JS ZIP writer** (zip.mjs, node:zlib) — standard zips with **zero native npm dependencies and no external commands**. - All file and subprocess work is asynchronous (`node:fs/promises` + `execFile`), with bounded concurrency (16 by default) for hashing and copying, so packing large trees never blocks DSH's event loop. ### Settings Web API (`/packer/api/*`): auth & limits The Settings tab is a peer of the `/pack` command and talks to the plugin's own prefix route, `/packer/api/*`. That route is **fail-closed** by default, and the checks run in the order "rate limit → auth → body size → routing", with all three gates completed **before the request body is buffered**: | Item | Default | Behaviour | | --- | --- | --- | | Auth | `api.authMode: 'auto'` | **Official mechanism only**: `requestRejection(req)` on `ctx.get('connection')` (dsh-client-connection) — the same Host/Origin trust plus signed browser session check the official `/api` channel uses. `401` → 401, `403` → 403, only `undefined` lets the request through | | Auth service unavailable | **Hard 403** | If the `connection` service is missing, has no `requestRejection`, or throws, the request is rejected with 403 and a reason; the plugin **never silently falls back** to a weaker path (the built-in token channel requires opting in with `authMode: 'token'`) | | Rate limit | 60 / 60 s | **Outermost**: a sliding window keyed by client address (`remoteAddress`), evaluated before auth and before reading the body, so unauthenticated traffic consumes the same quota. Over the limit: 429 with `retry-after`; the key table is bounded (no timers, bounded memory) | | Body size limit | 8 MB | Decided **before the body is fully buffered**: a declared `Content-Length` over the limit is rejected with 413 without reading a single byte; without a length the body is counted while streaming and the connection is unbound, paused and answered with 413 the moment it exceeds the limit | | Error messages | Redacted | Server-side absolute paths (drive letter / UNC / Unix home dirs) are replaced with `<路径已隐去>` before leaving the process, so directory layout is never echoed to the browser | `api` options (`apply(ctx, { api: { ... } })`): | Option | Default | Notes | | --- | --- | --- | | `authMode` | `'auto'` | `'auto'`: official `connection` only, 403 when unavailable; `'token'`: **explicit opt-in** plugin-owned fallback — Host / Origin / `Sec-Fetch-Site` same-origin checks plus a one-time token (generated at `apply` time, injected into the same-origin `index.html` via `webServer.tapIndex`; the UI sends it as the `x-dsh-packer-token` header), for hosts that genuinely have no `connection` service; `'off'`: **insecure**, isolated tests only | | `maxBodyBytes` | `8388608` | Body size limit in bytes | | `rateLimit` / `rateWindowMs` | `60` / `60000` | Rate-limit count and window (ms) | | `token` | random | Only used with `authMode: 'token'`; pass one explicitly to pin it for multi-instance or test setups | `authMode: 'off'` and an explicitly pinned `token` hand the security decision to the deployer; they are documented so you know what you opted into, not because they are recommended. ## Restore & diff Restore flow: 1. **Import the zip** — pick the file in the Settings tab, or `/pack restore `. 2. **Archive validation** — members are listed and whitelisted first (absolute / drive-letter / UNC / `..` / link-type members rejected), then `manifest.json` is read. 3. **Manifest validation** — `manifest.json` exists, its schemaVersion is compatible, every entry has a well-formed SHA-256 fingerprint, and every source file's hash matches; any mismatch is **fail-closed**. 4. **Diff report** — added / changed / same / skipped counts and file lists. 5. **Pick a conflict strategy**: - `overwrite` — replace the target file with the pack's content (default); - `skip` — keep the target file and skip conflicting entries; - `merge` — for text files, **append** the pack's content to the end of the target behind a separator comment; existing content is never overwritten. Non-text files fall back to overwrite. 6. **Backup → atomic swap → rollback on failure** — every target that will be replaced or appended to is backed up to `/.restore-backups//` first, and the pack's content is written to a temp file and swapped in with an atomic `rename`; **any failure aborts the batch** and rolls back everything this run replaced or added (counts return to zero and the rolled-back number is reported separately). 7. Apply, and restart DSH if needed. Files already identical to the pack are skipped automatically under every strategy. **Merge is not supported for structured configs** (JSON/YAML) — appending corrupts them; use overwrite or merge manually. Live SQLite databases (`*.db*`) are skipped by default to avoid overwriting memory data. ## `/pack` command reference ```text /pack list # list packs (encrypted ones are marked) /pack create [--modules skills,memory] [--mode migrate|share] [--note note] [--dry-run] [--encrypt passphrase] /pack create --share # --share is shorthand for --mode share /pack restore [--strategy overwrite|skip|merge] [--modules a,b] [--include-db] [--password passphrase] /pack scan # privacy-scan every packable module ``` | Command | Arguments | Description | | --- | --- | --- | | `list` | — | Lists generated packs: creation time, size, included modules, note, encryption flag | | `create` | `--modules a,b` pick modules by name; `--mode migrate\|share` (default `migrate`); `--share` shorthand; `--note note`; `--dry-run` preview only; `--encrypt passphrase` | Without `--modules`, modules follow the mode preset (migrate = all; share = Skills only) | | `restore` | ``; `--strategy overwrite\|skip\|merge`; `--modules a,b` restore only those modules; `--include-db` also restore the memory database (stop DSH first); `--password passphrase` | Import → validate → diff → apply with the chosen strategy; encrypted packs require the passphrase | | `scan` | — | Runs the privacy scan over every packable module and reports sensitive traces | Pack verification no longer has its own command: the settings page's "Browse" recomputes SHA-256 while listing files and flags mismatches in red. **Output directory**: packs are written to `~/.dsh/packs/` (override with `DSH_PACKS_DIR`); names look like `dsh-packer---.zip` (the unique suffix keeps same-second packs from overwriting each other), and each pack also gets a same-name `.json` summary file in the same directory (time / modules / note / file count) for the list view and quick identification. Restore backups go to `.restore-backups/-/` in that same directory (safe to delete at any time; neither the list nor the plugin reads it). Pack list, delete and rename are also available in the Settings → "Config Packer" tab. ## Configuration & environment variables | Variable | Default | Description | | --- | --- | --- | | `DSH_PACKS_DIR` | `~/.dsh/packs` | Pack output directory | | `DSH_MEMORY_ROOT` | `~/.dsh/memory` | Location of the `memory` module data | | `DSH_HOME` | `~/.dsh` | DSH data root (base for all module paths) | The Settings page is one page in five blocks, following the packer's task chain: **existing packs → contents to pack → pack options → restore (with per-item diff) → result details**. Pack info, integrity verification, rename and delete live on the pack row (delete sends the pack to the platform's own trash — on HarmonyOS it is permanent and announced first); restore shows the diff first, then module selection and strategy, and the result block reports the backup directory with a one-click rollback. Chinese-only copy, no icon characters, statuses in words. ## Compatibility - **Node.js** ≥ 22.19.0 - **Cross-platform**: Windows / macOS / Linux / HarmonyOS share one code base and one pack format. Trash is dispatched per platform (Windows Recycle Bin / macOS Trash / Linux freedesktop Trash); HarmonyOS still has two limits (trash and system file picker) — see [docs/platform-support.md](docs/platform-support.md). - **DSH version gate**: `engines.dsh` = `>=0.2.0-rc.2 <0.3.0-0` (tested on the 0.2.1-rc.3 desktop app). DSH evaluates compatibility with `includePrerelease`, so prerelease identifiers participate in range checks; DSH versions outside this range are refused by the plugin manager before install. - **Peer dependencies**: `@deepseek-ai/cordis` ^4.0.2 (plugin lifecycle baseline, provided by the host); `@deepseek-ai/dsh-tools` / `@deepseek-ai/dsh-session` share the `>=0.2.0-rc.2 <0.3.0-0` range and are **not imported by `index.mjs`** — the plugin only uses host services such as `ctx.commands` / `ctx.webServer` / `ctx.slots`, so both are marked `optional: true` in `peerDependenciesMeta`. - **No external commands for packing**: packs are read and written directly by zip.mjs (node:zlib) — creating, listing and extracting never call tar, bsdtar or unzip, and any standard unzip tool can open the produced packs. (Only the trash feature shells out to OS commands: PowerShell on Windows, osascript on macOS; Linux uses pure Node.) ## Version history | Version | Date | Type | Highlights | | --- | --- | --- | --- | | **v0.7.2** | 2026-10-28 | **Fixes the false "pack failed (network · Failed to fetch)"** | On a full migration (17469 files) the preview response was ~3.9 MB (3.5 MB of that the manifest) and the terminal poll still carried 2000+ privacy findings. When the host bridge dropped either large response the UI reported `network · Failed to fetch` even though the server had already written the pack (measured: a 218 MB zip with 17470 valid entries). Three fixes: (1) preview returns the first 1000 file entries plus `filesTotal` (the panel only renders the first 200 anyway); (2) the poll view replaces `result.privacy` with `privacyCount` (the create flow only reads `pack` / `warnings`); (3) the client now retries polls on network errors / 5xx with backoff (up to 10 tries, ≤5 s apart) instead of treating one hiccup as a failed pack. | | **v0.7.1** | 2026-10-28 | **Tests shipped with the package** | `files` now includes `tests`, plus a new `tests/privacy.test.mjs` (zero dependencies, `node --test`) that pins the behaviour this release promises: shebang lines survive untouched, Unix system paths (`/usr`, `/etc`, ...) are neither reported nor rewritten, home directories and Windows user directories are sanitized, URLs are left whole, and `SCHEMA_VERSION` stays at 1 so older packs still restore. Previously the manifest declared `npm test` while the tarball carried no tests, so it failed right after install. | | **v0.7.0** | 2026-10-27 | **Engineering refactor + privacy-scope correction + accessible UI** | (1) **Backend layering**: `index.mjs` drops from 3204 to 306 lines (assembly and re-exports only), split into `lib/{constants,errors,logger,util,modules,privacy,recycle,archive,pack,restore,jobs,api,cli}.mjs`, with privacy rules collapsed into a **single rule source** shared by scanning and sanitizing (they no longer keep two divergent regex sets). (2) **Privacy scope corrected**: the Unix path rule now matches **home directories only** (`/home`, `/Users`, `/root`); `/usr`, `/etc`, `/var`, `/opt`, `/srv`, `/mnt`, `/media` and `/tmp` are shared system paths with no personal information, so they are no longer reported or rewritten — previously a `#!/usr/bin/env node` shebang was turned into `#! node` and the script was destroyed. Shebang lines and URLs are now protected ranges. (3) **Frontend engineering**: `lib/client.js` stops being "a build artifact maintained as source" and becomes **TypeScript source plus a reproducible esbuild build** (`src/client/`: `copy` / `styles` / `types` / `format` / `api` / `hooks` / `model` / `ui` / `view/*`); `npm run typecheck` is clean. (4) **Accessibility**: dialogs gain `role=dialog` + `aria-modal` + `aria-labelledby` + Escape-to-close + a Tab focus trap + focus restore on close; the progress bar is a `role=progressbar` that omits `aria-valuenow` in the indeterminate state; table headers carry `scope=col`; async results are announced via `aria-live`. (5) **Fixes**: the ZIP classic-format 65535-entry limit now raises a readable error instead of writing a broken archive; SQLite snapshot directories no longer leak after `/scan`; the missing `passwordEmpty` copy string is added. Tests 91 → 102 (new artifact smoke tests and real jsdom render tests). (6) **Sanitize gaps closed + scan cache**: a new "mount / derived path" rule now rewrites `/Volumes/`, `/System/Volumes/Data`, `/private` and `/nix/store` — previously these were missed entirely, or only replaced up to the home directory, leaving half-sanitized output such as `/System/Volumes/Data<用户目录>/…`; the user-directory rule now consumes the mount prefix as well. Privacy scan results are cached by **file name + content SHA-256**, so "Preview" followed by "Pack" no longer scans the same files twice (any content change invalidates the entry at once). The UI spec is brought in line with the v0.7.0 structure (`docs/ui-spec.md`, replacing `ui-v060-spec.md`). Tests 103 → 106. | | **v0.6.5** | 2026-10-25 | **Session images/attachments now travel with migrations** | (1) New `attachments` module (root `~/.dsh/attachments`, `skipDirs: ['tmp']`, on by default for migration, never for sharing): session logs only hold an `{"type":"image","attachment":{"attachmentId":"sha256:…"}}` reference, while the bytes live under `attachments/v1/objects//` (content-addressed, deduplicated, append-only) — a pack with `sessions` alone leaves every session image broken after a restore. (2) `SENSITIVE_MODULES` gains `attachments`, so share mode is blocked server-side (not merely greyed out in the UI). (3) The "Session records" group in the settings page becomes `["sessions","attachments"]` (without it the backend module exists but the UI cannot select it). (4) `MEMORY_ROOT` now follows `DSH_HOME` (it used `os.homedir()`, diverging from memoryDb). (5) The module table gains `attachments` (plus the missing `plugins` row) and documents that session images live in `attachments` and the "Session records" group packs them; objects are content-addressed, so no reference rewriting is needed on restore. Tests 87 → 91. | | **v0.6.4** | 2026-10-25 | **Per-platform trash + manual path fallback** | (1) **Trash is dispatched per platform** (`recycleFileNative` contract unchanged: `true` on success, `false` when the platform has none, throw on failure → `deletePack` degrades and reports a warning): Windows keeps PowerShell `SendToRecycleBin`; macOS uses `osascript -e 'tell application "Finder" to delete POSIX file ""'` (quote/backslash escaping, 30s timeout); Linux implements the freedesktop Trash spec in pure Node — the file moves to `$XDG_DATA_HOME/Trash/files/` (defaults to `~/.local/share`, `.1` suffix on collisions) and `Trash/info/.trashinfo` records `Path=` (URL-encoded absolute path) plus `DeletionDate=` (local time `YYYY-MM-DDThh:mm:ss`); HarmonyOS and everything else still return `false` (permanent delete + warning). (2) **Manual path**: the restore page keeps its "or type a pack path" entry; `/status` gains three platform capability fields (`platform` / `trash` / `filePicker`), so platforms without a system file picker (HarmonyOS) show "当前平台请直接填写包路径" and treat manual input as the primary entry, while the delete confirmation tells the truth (not recoverable). The web layer cannot obtain host paths, so no platform renders a dead picker button. (3) Path examples are platform-specific (`/storage/…`, `/home/…`, `/Users/…`). Tests 80 → 87. | | **v0.6.3** | 2026-10-25 | Docs / pack scope | **Cross-platform docs now ship inside the package**: `files` gains `docs`, so an installed plugin carries `docs/platform-support.md` (support matrix for Windows / macOS / Linux / HarmonyOS, plus the two limits: Recycle Bin and system file picker). No code or behavior change. | | **v0.6.2** | 2026-10-25 | Intro / copy | Rewrote the intro line; tested desktop version is now 0.2.1-rc.3. No functional change | | **v0.6.1** | 2026-10-25 | **Plugin card copy rewritten** | Localized title ("Config Packer") and one-line summary via `locale/en.json` / `locale/zh.json` `meta`; `description` no longer embeds the version or lists features. No functional changes. | | **v0.6.0** | 2026-10-25 | **Settings page rebuilt around the packer's job** | The UI is no longer a form of module cards and buttons; it is one page following the task chain, in five blocks: **existing packs** (name / time / contents / size / encryption, with restore · pack info · delete — delete goes to the Windows Recycle Bin) → **contents to pack** (plain-language list: skills / installed plugins / memory database / sessions / config & presets, with sizes and a total) → **pack options** (as-is / sanitize before packing / encrypt, with passphrase, scan verdict, progress and cancel) → **restore** (pick a pack or a path → per-item diff: in pack vs on this machine vs how to handle → module selection + memory-db toggle → strategy) → **result details** (overwritten / added / skipped / needs confirmation / failed + backup directory + one-click rollback). Naming now follows the plugin itself (no more box/seal/unseal metaphors); Chinese-only copy, **no emoji or icon characters**, statuses expressed in words. `lib/client.js` 1048 → 1421 lines. Backend adds two fields: `modules` and `sha256` per pack in `GET /packs`, and `pack.sha256` in the create result. Tests 80/80. | | **v0.5.1** | 2026-10-25 | **Privacy actually handled + plugins travel with migrations** | (1) Privacy goes from "report only" to "must handle": the pack page offers **Sanitize before packing** (rewrites only the copy inside the pack — paths become `<本机路径>` / `<用户目录>` / `` / ``, credentials become `<已移除的密钥>`; source files are never touched; forced in share mode) and **Pack as-is** (faithful; warns when credentials are included without a passphrase). (2) Five safety gates for sanitizing: single left-to-right pass, structural characters excluded, any hit containing `{ } [ ] , ; " '` is skipped with a warning, JSON/JSONL is re-parsed with line-shape checks (whole file reverted on failure), non-UTF-8 files are never sanitized, and size/sha256 are recomputed after rewriting (otherwise restore would reject the pack as tampered). (3) New `plugins` module packs the **installed plugin bodies** (`profiles/*/node_modules` top-level packages, scoped `@scope/*` handled, pnpm links followed, cycles guarded) — on by default for migration, never for sharing. (4) Restore back-fills sanitized packs (`<用户目录>` → target home, `<用户名>` → current user) and reports retained placeholders in `stats.warnings`. (5) `/scan` findings carry a `kind` (7 values). Tests 80/80. | | **v0.5.0** | 2026-10-25 | **Slimming (small is beautiful)** | Deletions only: (1) the built-in trash bin is gone (3 endpoints + a panel) — deleted packs now go to the **Windows Recycle Bin**; (2) scheduled backups, the retention policy and their config file are gone (no scheduler); (3) the standalone `verify` endpoint and command are gone — verification is folded into "Browse", which recomputes SHA-256 while listing files and flags mismatches; (4) `/status` drops `trashCount` / `retention` / `autoBackup` / `disk.trashBytes`; (5) the UI goes from three tabs to **two (Pack / Restore)** with pack management moved onto the Restore pack row, the whole English copy table is removed (Chinese-only personal tool), and `lib/client.js` shrinks from 2334 to ≤1100 lines; (6) the result card gains a "Copy path" action. Endpoints: 15 → 9 groups. Kept: progress & cancel, encrypted packs (one toggle + passphrase), selective restore, `includeDb`, restore backups with one-click rollback, privacy scan and share guard. | | **v0.4.0** | 2026-10-25 | **Feature completion + UI rebuild + three broken security promises fixed** | (1) **False-negative scan fixed**: the settings page's "Scan" used to downgrade the file list to `{module,rel,size}` and drop absolute paths, so `privacyScan` could not read any file and always reported "no sensitive traces"; `abs` is passed again. (2) **Share protection now enforced server-side**: `share` mode rejects `sessions`/`profiles`/`settings`/`presets`/`memory`/`memoryDb` with `400 share_scope` instead of trusting the UI. (3) **Memory database can be restored**: `includeDb` is now reachable from a UI toggle and `--include-db` (previously API-only). (4) `/status` no longer runs `VACUUM INTO` on the memory DB (the old code leaked a ~2.9 MB snapshot per settings-page open). (5) `strategy`/`mode` are whitelisted fail-closed (invalid values used to degrade silently to overwrite). (6) New: background jobs with progress/cancel, selective restore, per-file diff, pack-content browsing, `/pack verify`, trash bin, AES-256-GCM encrypted packs, scheduled backups + retention, one-click rollback of a restore. (7) UI rebuilt from one long page into **three tabs (Pack / Library / Restore)** with custom input and confirm dialogs (`window.prompt`/`confirm` removed — Electron does not support `prompt`, so Rename never worked in the desktop app). 74/74 tests pass. | | **v0.3.0** | 2026-09-29 | Bug fix | **Missing module in the UI fixed**: the settings page hard-coded a 6-entry module list, so the `memoryDb` module added in v0.2.6 (the SQLite truth-source snapshot) **could not be selected at all**. The list is now driven entirely by the host's `/status` payload (`moduleOrder(status.modules)`), and default selection iterates every module the host reports. Two consistency guards were added to the test suite (the client must not hard-code a module list; every host module must be renderable by the client). 50/50 tests pass. | | **v0.2.6** | 2026-09-29 | Feature / adaptation | **The memory truth source is now inside the pack (consistent snapshot) + DSH 0.2.0-rc.2 adaptation**, superseding the v0.2.5 limitation: (1) new `memoryDb` module — the runtime source of truth (`/biomemory.db`) is packed as a `VACUUM INTO` snapshot taken over a read-only connection (never a copy of a live WAL database; measured 2.78MB → 2.57MB, entries and audit rows intact); a failed snapshot leaves the file out and reports it under `unreadable` rather than silently degrading; snapshots live in a temp directory and are always cleaned up (success, failure and `--dry-run` alike); (2) dropped `@deepseek-ai/dsh-client-runtime` from `dsh.client.inject` — that package does not exist in 0.2.0. 48/48 tests pass | | **v0.2.5** | 2026-09-20 | Semantics | **The "memory" in a pack is only the mirror**: the `memory` module packs the read-only Markdown copy under `~/.dsh/memory`, while the runtime source of truth is SQLite (`~/.dsh/biomemory/biomemory.db`) — excluded by `skipFiles` and never scanned (a live WAL database cannot be copied safely). The module label now reads "memory mirror (…; the live SQLite database is not in the pack)" and both READMEs gained a **How memory migrates** section: **stop DSH first**, then copy `biomemory.db` (plus `-wal`/`-shm`) by hand — the pack alone gives the target only the mirror. Also: client bundle moved to `lib/client.js`. 47 tests green | | **v0.2.4** | 2026-09-17 | Async / security | File and subprocess work is async end to end (`node:fs/promises` + `execFile`, streaming hashes, bounded concurrency of 16 for hashing and copying, `sha256()` now throws on failure); the public API returns Promises and no longer blocks the event loop; new `/packer/api/*` protections: fail-closed auth (official `connection.requestRejection` only — a missing service, missing method or a throwing call is a hard 403), an explicitly opted-in one-time-token fallback (`authMode: 'token'`, same-origin checks + `webServer.tapIndex` injection), a rate limit (60/min, outermost gate) and a body size limit (8 MB, decided before the body is buffered), plus path redaction in error messages; `apply()` now explicitly wires the handler into `webServer.register({ kind: 'prefix', path: '/packer/api' })`; tests grown to 47 cases (unauthorized 403 / oversized 413 / rate-limited 429 / token path / no-connection default deny / apply wiring) | | **v0.2.3** | 2026-09-16 | Security hardening | Target-path containment plus a `rel` whitelist on restore (absolute / drive-letter / UNC / `..` rejected); integrity made fail-closed (missing or malformed fingerprints rejected, `sha256()` now throws instead of returning `''`, streaming hash); archive members whitelisted via `tar -tf` before unpacking, link-type members refused, temp dirs always cleaned in `try/finally`; restore now backs up → temp file → atomic `rename` → abort-and-rollback on failure; `memory` module excludes `*.db*` by default; privacy scan gained Unix/UNC paths, unquoted credentials and bare key shapes, `.env`-style extensionless text, with per-line full counting; unique pack-name suffix; `peerDependenciesMeta` marks host-provided peers optional; the web UI now surfaces server-side error reasons | | **v0.2.2** | 2026-09-05 | Adaptation / UI | Adapted for DSH 0.1.2-rc.1; management panel redesigned on the "skeleton / flesh / breath" design language — packing-workflow layout (stage progress bar / equipment panel / diff color bands) + orange-amber-teal brand palette (packing & migration) + dark-mode support (follows DSH theme, dual-channel detection) | | **v0.2.1** | 2026-09-05 | UI refactor | Config Packer panel UI rebuilt — neutralSurface background + white cards (max-width 860 centered, radius 16), 4/8px grid spacing, restrained 150ms transitions; colors strictly from dsh-fuse default tokens (`--pk-*` variables, zero hardcoded values); diff report got four-column count badges + semantic color dots (added=green / changed=orange / same+skipped=muted); privacy risks default to a warning tint | | **v0.2.0** | 2026-09-05 | Security hardening | Privacy scan fixed: merged personal rules now actually participate in the loop; every source file is verified against its manifest SHA-256 before restoring (fail-closed); restore path containment (zip-slip / tampered-manifest escapes rejected); append-merge refused for structured configs (JSON/YAML — it corrupts them) | | **v0.1.2** | — | Metadata / deps | package.json gained `keywords` / `files` metadata; peerDeps relaxed to ≥0.1.1-rc.2; README version & dependency notes synced | | **v0.1.0** | — | Initial release | Modular packing (skills / sessions / profiles / settings / presets / memory), privacy scan, restore diff, pack management, Settings panel | ## FAQ **The zip won't open / looks corrupted?** Packs are standard zips created by the plugin pure-JS ZIP writer — Windows Explorer and common unzip tools can open them. If a pack fails validation, don't hand-edit its contents (that breaks the SHA-256 fingerprints in `manifest.json`); regenerate it with `/pack create`. Check `~/.dsh/packs` (or your `DSH_PACKS_DIR`) with `/pack list` to see what's there. **Manifest validation fails on restore?** Usually one of: the zip was not created by dsh-packer (no `manifest.json` inside), `manifest.json` is missing or its schemaVersion is incompatible with the current version, or the pack was modified after creation. Redistribute the original pack or regenerate it. **My share pack got blocked — what now?** Share mode does **not** abort on privacy hits — hits are always sanitized before packing. Only two things fail a share pack: selecting a module the share preset forbids (only `skills` is allowed; anything else returns `share_scope`), and files whose content cannot be read and therefore never went through the scan (fail-closed — the response lists them). For the first, drop the module that should not be shared; for the second, fix those files (permissions, lock, corruption) and retry. Use migrate mode when you need a faithful backup (report-only, no forced sanitizing). **How does the merge strategy work?** For text files, the pack's content is **appended** to the target file behind a separator comment — existing content is never overwritten. Non-text files fall back to overwrite; structured configs (JSON/YAML) don't support merging at all (appending corrupts them) — use overwrite or merge manually. Files already identical to the pack are skipped under every strategy. **What does "aborted / rolled back" on restore mean?** Restore is all-or-nothing: if any step (path validation, fingerprint check, writing) fails, it stops immediately and rolls back the files this run already replaced or added, using the backups taken moments earlier. `rolled back N` reports how many files were restored; failure reasons are listed entry by entry in the Settings tab. Backups stay under `/.restore-backups/` for manual inspection. **Why wasn't my memory database (`*.db*`) restored?** Live SQLite databases are held open by DSH / the memory plugin, so overwriting them can corrupt data. The plugin neither packs nor restores `*.db`, `*.db-wal`, `*.db-shm` or `*.sqlite` by default; on restore they are counted as skipped with an explicit reason. To actually move memory across: **quit DSH first**, then enable the `includeDb` toggle on the restore tab (or `/pack restore --include-db`). The packed database comes from a consistent `VACUUM INTO` snapshot (the `memoryDb` module), so it is complete and usable. **Restore says "entry is missing a valid SHA-256 fingerprint"?** The pack was not generated by dsh-packer, or its `manifest.json` was hand-edited. Missing or malformed fingerprints are rejected outright (fail-closed) — redistribute the original pack or regenerate it. **Where are packs stored?** `~/.dsh/packs` by default, overridable via the `DSH_PACKS_DIR` environment variable; each pack also has a same-name `.json` summary file in the same directory for identification and the list view. ## Development ```bash npm test # node --test "tests/*.test.mjs", 37 tests, all green node scripts/release-check.mjs # release consistency check (version / README version exposure / files whitelist / git state) ``` **CI**: `.github/workflows/ci.yml` runs "release check → unit tests → `npm pack --dry-run`" on Node 22.x / 24.x (this package has zero runtime dependencies, so there is no install step) for every push and pull request. ## License MIT — see [LICENSE](LICENSE).